<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Rate Limiting on WebDevStation</title>
    <link>https://webdevstation.com/tags/rate-limiting/</link>
    <description>1 article tagged Rate Limiting — tutorials, code examples and notes from building real systems, newest first.</description>
    <generator>Hugo</generator>
    <language>en</language>
    <managingEditor>Alex</managingEditor>
    <webMaster>Alex</webMaster>
    <copyright>© 2026 WebDevStation</copyright>
    <lastBuildDate>Tue, 25 Aug 2026 09:00:00 +0200</lastBuildDate>
    <atom:link href="https://webdevstation.com/tags/rate-limiting/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Rate Limiting Go APIs with golang.org/x/time/rate</title>
      <link>https://webdevstation.com/posts/rate-limiting-go-apis/</link>
      <pubDate>Tue, 25 Aug 2026 09:00:00 +0200</pubDate>
      <author>Alex</author>
      <guid isPermaLink="true">https://webdevstation.com/posts/rate-limiting-go-apis/</guid>
      <description>Protect your Go HTTP APIs with token bucket rate limiting: per-client limiters, middleware, the standard rate limit headers, client-side throttling and when to move…</description>
      <content:encoded><![CDATA[<p>Somebody eventually points a badly written script at your API. Not maliciously — usually it is a colleague&rsquo;s retry loop with no back-off, or a cron job that fires every minute and takes ninety seconds. Without a limit, one client can consume the capacity you were saving for everyone else. <code>golang.org/x/time/rate</code> handles this in about as much code as it takes to describe, and it is the piece I now add before the first public endpoint ships.</p>
<h2 id="the-token-bucket-in-one-paragraph">The Token Bucket, in One Paragraph</h2>
<p>Picture a bucket that holds <code>b</code> tokens and refills at <code>r</code> tokens per second. Every request takes one token. If the bucket is empty, the request is rejected (or waits). That is the whole model, and it has one property that makes it the right default: <code>b</code> is a <strong>burst</strong> allowance. A client that has been idle can spend its accumulated tokens all at once, then settles into the steady rate. Real traffic is bursty — a page load firing six API calls should not be punished, while a loop firing six hundred should.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;golang.org/x/time/rate&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// 10 requests per second, bursts of up to 20.</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">limiter</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">rate</span>.<span style="color:#a6e22e">NewLimiter</span>(<span style="color:#ae81ff">10</span>, <span style="color:#ae81ff">20</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">limiter</span>.<span style="color:#a6e22e">Allow</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// over budget</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Three methods, for three different situations:</p>
<table>
	<thead>
			<tr>
					<th>Method</th>
					<th>Behaviour</th>
					<th>Use it for</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>Allow()</code></td>
					<td>Returns immediately: true or false</td>
					<td>Inbound HTTP — reject with 429</td>
			</tr>
			<tr>
					<td><code>Wait(ctx)</code></td>
					<td>Blocks until a token is free or ctx ends</td>
					<td>Outbound calls you control</td>
			</tr>
			<tr>
					<td><code>Reserve()</code></td>
					<td>Reserves a token, tells you the delay</td>
					<td>When you need to report <code>Retry-After</code></td>
			</tr>
	</tbody>
</table>
<p><code>rate.Limit</code> is a float, so fractional rates work: <code>rate.Every(time.Minute/100)</code> is 100 per minute, and <code>rate.Limit(0.5)</code> is one request every two seconds. <code>rate.Inf</code> disables limiting entirely, which is handy for a per-plan configuration where some tier is unlimited.</p>
<h2 id="a-global-limiter-is-not-enough">A Global Limiter Is Not Enough</h2>
<p>The naive version puts one limiter in front of everything:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">global</span> = <span style="color:#a6e22e">rate</span>.<span style="color:#a6e22e">NewLimiter</span>(<span style="color:#ae81ff">100</span>, <span style="color:#ae81ff">200</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">limit</span>(<span style="color:#a6e22e">next</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span>) <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandlerFunc</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">global</span>.<span style="color:#a6e22e">Allow</span>() {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#a6e22e">w</span>, <span style="color:#e6db74">&#34;too many requests&#34;</span>, <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">StatusTooManyRequests</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">next</span>.<span style="color:#a6e22e">ServeHTTP</span>(<span style="color:#a6e22e">w</span>, <span style="color:#a6e22e">r</span>)
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This protects your <em>server</em> but not your <em>users</em>: one aggressive client can still eat the entire global budget and everyone else gets 429s. A global limiter is a useful backstop, not a fairness mechanism. What you want is a limiter per client, with the global one behind it.</p>
<h2 id="per-client-limiters">Per-Client Limiters</h2>
<p>Keep a map from client key to limiter, guarded by a mutex, with a janitor that evicts idle entries so the map does not grow forever.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">ratelimit</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;sync&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;time&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;golang.org/x/time/rate&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">client</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">limiter</span>  <span style="color:#f92672">*</span><span style="color:#a6e22e">rate</span>.<span style="color:#a6e22e">Limiter</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">lastSeen</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Store hands out one limiter per key and forgets keys that go quiet.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Store</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">mu</span>      <span style="color:#a6e22e">sync</span>.<span style="color:#a6e22e">Mutex</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">clients</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#f92672">*</span><span style="color:#a6e22e">client</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">rate</span>    <span style="color:#a6e22e">rate</span>.<span style="color:#a6e22e">Limit</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">burst</span>   <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ttl</span>     <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewStore</span>(<span style="color:#a6e22e">r</span> <span style="color:#a6e22e">rate</span>.<span style="color:#a6e22e">Limit</span>, <span style="color:#a6e22e">burst</span> <span style="color:#66d9ef">int</span>, <span style="color:#a6e22e">ttl</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">Store</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">Store</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">clients</span>: make(<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#f92672">*</span><span style="color:#a6e22e">client</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">rate</span>:    <span style="color:#a6e22e">r</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">burst</span>:   <span style="color:#a6e22e">burst</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">ttl</span>:     <span style="color:#a6e22e">ttl</span>,
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">go</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">cleanup</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Limiter returns the limiter for key, creating it on first use.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Store</span>) <span style="color:#a6e22e">Limiter</span>(<span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">rate</span>.<span style="color:#a6e22e">Limiter</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Lock</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Unlock</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">c</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">clients</span>[<span style="color:#a6e22e">key</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">c</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">client</span>{<span style="color:#a6e22e">limiter</span>: <span style="color:#a6e22e">rate</span>.<span style="color:#a6e22e">NewLimiter</span>(<span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">rate</span>, <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">burst</span>)}
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">clients</span>[<span style="color:#a6e22e">key</span>] = <span style="color:#a6e22e">c</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">lastSeen</span> = <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">limiter</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Store</span>) <span style="color:#a6e22e">cleanup</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ticker</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">NewTicker</span>(<span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">ttl</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">ticker</span>.<span style="color:#a6e22e">Stop</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">ticker</span>.<span style="color:#a6e22e">C</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Lock</span>()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">clients</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Since</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">lastSeen</span>) &gt; <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">ttl</span> {
</span></span><span style="display:flex;"><span>                delete(<span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">clients</span>, <span style="color:#a6e22e">key</span>)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Unlock</span>()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The mutex is not optional. A bare <code>map[string]*rate.Limiter</code> written from concurrent handlers is a textbook data race, and Go&rsquo;s runtime will happily crash the process with <code>concurrent map writes</code> — the same failure I dug into in <a href="/posts/concurrent-map-writing-and-reading-in-go/">concurrent map writing and reading in Go</a>.</p>
<p>Two design notes. <code>sync.Map</code> is not a better fit here: it is optimised for read-mostly workloads with stable keys, and this map is written on every new client. And an unbounded map is a memory-exhaustion vector if the key is attacker-controlled — hence the TTL. For a hard cap, put an LRU in front of it.</p>
<h2 id="the-middleware">The Middleware</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">Middleware</span>(<span style="color:#a6e22e">store</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Store</span>) <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span>) <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">next</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span>) <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandlerFunc</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">limiter</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">store</span>.<span style="color:#a6e22e">Limiter</span>(<span style="color:#a6e22e">clientKey</span>(<span style="color:#a6e22e">r</span>))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Reserve, rather than Allow, so we can report Retry-After.</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">res</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">limiter</span>.<span style="color:#a6e22e">Reserve</span>()
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">OK</span>() {
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Burst is smaller than the request size; never satisfiable.</span>
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#a6e22e">w</span>, <span style="color:#e6db74">&#34;rate limit misconfigured&#34;</span>, <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">StatusInternalServerError</span>)
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">delay</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">Delay</span>(); <span style="color:#a6e22e">delay</span> &gt; <span style="color:#ae81ff">0</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// We are not going to wait, so give the token back.</span>
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">Cancel</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">Header</span>().<span style="color:#a6e22e">Set</span>(<span style="color:#e6db74">&#34;Retry-After&#34;</span>, <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">Itoa</span>(int(<span style="color:#a6e22e">math</span>.<span style="color:#a6e22e">Ceil</span>(<span style="color:#a6e22e">delay</span>.<span style="color:#a6e22e">Seconds</span>()))))
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">Header</span>().<span style="color:#a6e22e">Set</span>(<span style="color:#e6db74">&#34;RateLimit-Limit&#34;</span>, <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">Itoa</span>(<span style="color:#a6e22e">limiter</span>.<span style="color:#a6e22e">Burst</span>()))
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">Header</span>().<span style="color:#a6e22e">Set</span>(<span style="color:#e6db74">&#34;RateLimit-Remaining&#34;</span>, <span style="color:#e6db74">&#34;0&#34;</span>)
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">Header</span>().<span style="color:#a6e22e">Set</span>(<span style="color:#e6db74">&#34;RateLimit-Reset&#34;</span>, <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">Itoa</span>(int(<span style="color:#a6e22e">math</span>.<span style="color:#a6e22e">Ceil</span>(<span style="color:#a6e22e">delay</span>.<span style="color:#a6e22e">Seconds</span>()))))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">WriteHeader</span>(<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">StatusTooManyRequests</span>)
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">NewEncoder</span>(<span style="color:#a6e22e">w</span>).<span style="color:#a6e22e">Encode</span>(<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>{
</span></span><span style="display:flex;"><span>                    <span style="color:#e6db74">&#34;error&#34;</span>: <span style="color:#e6db74">&#34;rate limit exceeded&#34;</span>,
</span></span><span style="display:flex;"><span>                })
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">next</span>.<span style="color:#a6e22e">ServeHTTP</span>(<span style="color:#a6e22e">w</span>, <span style="color:#a6e22e">r</span>)
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>res.Cancel()</code> is the line people forget. <code>Reserve</code> takes the token immediately; if you then decide not to wait, cancelling returns it to the bucket. Skip it and every rejected request still consumes budget, so a client that trips the limit stays locked out far longer than intended.</p>
<p>This plugs into any router the same way as the handlers in my <a href="/posts/go-middleware-example/">Go middleware example</a>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">store</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ratelimit</span>.<span style="color:#a6e22e">NewStore</span>(<span style="color:#a6e22e">rate</span>.<span style="color:#a6e22e">Limit</span>(<span style="color:#ae81ff">10</span>), <span style="color:#ae81ff">20</span>, <span style="color:#ae81ff">10</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Minute</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">r</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">chi</span>.<span style="color:#a6e22e">NewRouter</span>()
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Use</span>(<span style="color:#a6e22e">ratelimit</span>.<span style="color:#a6e22e">Middleware</span>(<span style="color:#a6e22e">store</span>))
</span></span></code></pre></div><h2 id="choosing-the-client-key">Choosing the Client Key</h2>
<p>This is where rate limiting is usually got wrong, and it is worth more thought than the algorithm.</p>
<p><strong>Authenticated requests: key on the identity.</strong> An API key or user ID is stable, meaningful, and cannot be spoofed once you have verified the token. If you are issuing JWTs — as in <a href="/posts/user-authentication-with-go-using-jwt-token/">user authentication in Go Echo with JWT</a> — the subject claim is your key.</p>
<p><strong>Anonymous requests: key on the IP, carefully.</strong> <code>r.RemoteAddr</code> behind a proxy is the proxy&rsquo;s address, so every user shares one bucket. But blindly trusting <code>X-Forwarded-For</code> is worse: it is a client-supplied header, and anyone can put whatever they like in it to get a fresh bucket per request.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">clientKey</span>(<span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) <span style="color:#66d9ef">string</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Authenticated callers are keyed on identity.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">userID</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">auth</span>.<span style="color:#a6e22e">UserFrom</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Context</span>()); <span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;user:&#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">userID</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Only trust the proxy header if the request came from our proxy,</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// and take the address the proxy appended — the rightmost hop.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ip</span>, <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">SplitHostPort</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">RemoteAddr</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">isTrustedProxy</span>(<span style="color:#a6e22e">ip</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">xff</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Header</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;X-Forwarded-For&#34;</span>); <span style="color:#a6e22e">xff</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;&#34;</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">parts</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Split</span>(<span style="color:#a6e22e">xff</span>, <span style="color:#e6db74">&#34;,&#34;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">ip</span> = <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">TrimSpace</span>(<span style="color:#a6e22e">parts</span>[len(<span style="color:#a6e22e">parts</span>)<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>])
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;ip:&#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">ip</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The rightmost entry is the one your own proxy added; everything to its left came from the client and is unverifiable. If your platform provides a trusted header — Cloudflare&rsquo;s <code>CF-Connecting-IP</code>, or the standard <code>Forwarded</code> from a proxy you control — prefer it.</p>
<p>One more refinement: not all endpoints are equal. <code>POST /reports/export</code> might cost a hundred times what <code>GET /health</code> does. <code>AllowN</code> and <code>ReserveN</code> let you charge by cost:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">cost</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Method</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">MethodPost</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">HasPrefix</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">URL</span>.<span style="color:#a6e22e">Path</span>, <span style="color:#e6db74">&#34;/reports&#34;</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cost</span> = <span style="color:#ae81ff">25</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">res</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">limiter</span>.<span style="color:#a6e22e">ReserveN</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>(), <span style="color:#a6e22e">cost</span>)
</span></span></code></pre></div><p>Just keep the burst at least as large as your most expensive operation, or <code>res.OK()</code> returns false forever and that endpoint becomes permanently unreachable.</p>
<h2 id="limiting-yourself-too">Limiting Yourself, Too</h2>
<p>Rate limiting is not only defensive. When you are the client of somebody else&rsquo;s API, respecting their limit proactively beats absorbing 429s and retrying. <code>Wait</code> is built for this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Client</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">http</span>    <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Client</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">limiter</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">rate</span>.<span style="color:#a6e22e">Limiter</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewClient</span>() <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">Client</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">http</span>:    <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Client</span>{<span style="color:#a6e22e">Timeout</span>: <span style="color:#ae81ff">10</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>},
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// The upstream allows 5 requests/second; stay under it.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">limiter</span>: <span style="color:#a6e22e">rate</span>.<span style="color:#a6e22e">NewLimiter</span>(<span style="color:#ae81ff">5</span>, <span style="color:#ae81ff">5</span>),
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) <span style="color:#a6e22e">Do</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Response</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Blocks until a token is available, or ctx is cancelled.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">limiter</span>.<span style="color:#a6e22e">Wait</span>(<span style="color:#a6e22e">ctx</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;rate limiter: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Do</span>(<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">WithContext</span>(<span style="color:#a6e22e">ctx</span>))
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>Wait</code> returns an error if the context is cancelled or if its deadline arrives before a token would — so a caller that has already given up never sits in the queue. That is the <a href="/posts/understanding-golang-context/">context</a> machinery doing exactly what it is for.</p>
<p>This composes neatly with a bounded worker pool: the pool caps how many requests are <em>in flight</em>, the limiter caps how many <em>start per second</em>. They constrain different things and you usually want both, as I covered in <a href="/posts/worker-pools-in-go-with-errgroup/">worker pools in Go with errgroup</a>.</p>
<h2 id="where-this-approach-stops-working">Where This Approach Stops Working</h2>
<p>Be honest about the limits of an in-process limiter.</p>
<p><strong>It is per instance.</strong> Three replicas with a limit of 10/s allow 30/s in total, and a client bouncing between them gets a fresh bucket each time. For a real global limit you need shared state — Redis with a Lua script that does the token accounting atomically, or a limiter at the edge.</p>
<p><strong>It is lost on restart.</strong> Every deploy resets every bucket. Usually fine; occasionally not.</p>
<p><strong>It costs you a request.</strong> The request still reaches your process, gets routed, and allocates before being rejected. Under a genuine flood, that is exactly the work you cannot afford — which is why volumetric protection belongs at the CDN or load balancer, not in your handler.</p>
<p>My rule of thumb: <strong>application limits enforce fairness and per-plan quotas; edge limits absorb abuse.</strong> They solve different problems and you want both. The nginx layer is a natural place for the coarse one, and it can be surprisingly nuanced — the <a href="/posts/how-to-make-nginx-cookie-aware/">cookie-aware caching</a> tricks work the same way for keying <code>limit_req</code> zones.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">limit_req_zone</span> $binary_remote_addr <span style="color:#e6db74">zone=api:10m</span> <span style="color:#e6db74">rate=100r/s</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">location</span> <span style="color:#e6db74">/api/</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">limit_req</span> <span style="color:#e6db74">zone=api</span> <span style="color:#e6db74">burst=200</span> <span style="color:#e6db74">nodelay</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://backend</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="testing-it">Testing It</h2>
<p>Rate limiting is easy to test badly, because <code>time.Now()</code> is involved. Keep the rates small and explicit rather than sleeping through real seconds:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestLimiterRejectsBurst</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">store</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ratelimit</span>.<span style="color:#a6e22e">NewStore</span>(<span style="color:#a6e22e">rate</span>.<span style="color:#a6e22e">Limit</span>(<span style="color:#ae81ff">1</span>), <span style="color:#ae81ff">3</span>, <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Minute</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">h</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ratelimit</span>.<span style="color:#a6e22e">Middleware</span>(<span style="color:#a6e22e">store</span>)(<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandlerFunc</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) { <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">WriteHeader</span>(<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">StatusOK</span>) },
</span></span><span style="display:flex;"><span>    ))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">codes</span> <span style="color:#f92672">:=</span> make([]<span style="color:#66d9ef">int</span>, <span style="color:#ae81ff">5</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">codes</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">req</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">httptest</span>.<span style="color:#a6e22e">NewRequest</span>(<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">MethodGet</span>, <span style="color:#e6db74">&#34;/&#34;</span>, <span style="color:#66d9ef">nil</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">RemoteAddr</span> = <span style="color:#e6db74">&#34;203.0.113.7:1234&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">rec</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">httptest</span>.<span style="color:#a6e22e">NewRecorder</span>()
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">ServeHTTP</span>(<span style="color:#a6e22e">rec</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">codes</span>[<span style="color:#a6e22e">i</span>] = <span style="color:#a6e22e">rec</span>.<span style="color:#a6e22e">Code</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Burst of 3 succeeds, the rest are rejected.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">want</span> <span style="color:#f92672">:=</span> []<span style="color:#66d9ef">int</span>{<span style="color:#ae81ff">200</span>, <span style="color:#ae81ff">200</span>, <span style="color:#ae81ff">200</span>, <span style="color:#ae81ff">429</span>, <span style="color:#ae81ff">429</span>}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">slices</span>.<span style="color:#a6e22e">Equal</span>(<span style="color:#a6e22e">codes</span>, <span style="color:#a6e22e">want</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;got %v, want %v&#34;</span>, <span style="color:#a6e22e">codes</span>, <span style="color:#a6e22e">want</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then confirm the behaviour under real load before you trust the number. Pointing a load test at the endpoint and watching the ratio of 200s to 429s tells you whether your limit matches the traffic you actually get — the setup in <a href="/posts/an-easy-way-to-loadtest-your-web-apps/">an easy way to load test your web apps</a> is enough for this.</p>
<h2 id="checklist">Checklist</h2>
<ul>
<li>Per-client limiters, not one global bucket, with a global one as backstop.</li>
<li>Key on identity when authenticated; on a <em>verified</em> IP otherwise.</li>
<li>Evict idle limiters so the map cannot grow without bound.</li>
<li><code>res.Cancel()</code> whenever you reject instead of waiting.</li>
<li>Send <code>Retry-After</code> and <code>RateLimit-*</code> headers so good clients can behave.</li>
<li>Burst at least as large as your most expensive weighted operation.</li>
<li><code>Wait(ctx)</code> on the client side of other people&rsquo;s APIs.</li>
<li>Volumetric protection at the edge, fairness in the application.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p><code>golang.org/x/time/rate</code> is one of those packages that does exactly one thing and does it without ceremony. The algorithm is not the hard part — picking a sensible client key, giving tokens back when you reject, and being clear about what an in-process limiter can and cannot promise is where the real work is. Get those right and a single misbehaving script stops being everybody else&rsquo;s problem. And if the API you are the client of happens to be a model provider, an agent loop is a remarkably efficient way to find its limits — see <a href="/posts/tool-use-in-go-agent-loop/">tool use in Go</a>.</p>]]></content:encoded>
      <category>Go Programming</category>
      <category>Web Development</category>
      <category>Backend Development</category>
      <category>Security</category>
    </item>
  </channel>
</rss>
