{"id":"rate-limiting","title":"Rate limiting","sections":[{"id":"overview","title":"Overview","html":"<p>A rate limiter caps how many requests a client can make in a period of time and turns away the rest. It protects shared capacity from one noisy client, blunts abuse like credential stuffing and scraping, keeps costs predictable when each request costs you money (SMS, third-party APIs, GPU time), and turns API pricing tiers into something you can enforce.</p>\n<p>Interviews use it two ways. \"Design a rate limiter\" is a classic standalone question, and it tests whether you can pick an algorithm, keep counters correct across many servers, and decide what happens when the limiter itself fails. Rate limiting also comes up as a component inside bigger designs: any public API, login flow or notification system should have one.</p>\n<p>It's related to <a href=\"/learn/asynchronism#back-pressure\">back pressure</a> in the primer, but the focus differs. Back pressure reacts to how loaded the server is. A rate limiter enforces a policy per client, whether or not the server is busy.</p>","headings":[]},{"id":"where-the-limiter-lives","title":"Where the limiter lives","html":"<h3 id=\"where-the-limiter-lives\">Where the limiter lives</h3>\n<p>You can enforce limits at several layers, and each one sees different information.</p>\n<ul>\n<li><strong>In the client.</strong> Polite SDKs throttle themselves, which cuts wasted requests. You can't rely on it, since an abusive client won't run your SDK.</li>\n<li><strong>At the edge</strong> (CDN, WAF, <a href=\"/learn/reverse-proxy-web-server#overview\">reverse proxy</a>). Rejecting here is cheapest, because the request never reaches your servers. But the edge usually knows only the IP address, path and headers, not who the user is or what plan they pay for.</li>\n<li><strong>At the API gateway.</strong> This is the most common home for per-user and per-API-key limits. The gateway has authenticated the caller, so it knows their identity and tier, and the limit is applied in one place for every service behind it.</li>\n<li><strong>In the service.</strong> Only the service knows how expensive an operation really is (\"export 10,000 rows\" versus \"fetch one row\"), so cost-based limits live here. The request has already used some resources by the time it's rejected.</li>\n<li><strong>In front of a shared dependency.</strong> Per-tenant connection pools or queue quotas stop one customer from monopolizing a database.</li>\n</ul>\n<p>Most production systems layer these: a coarse per-IP limit at the edge to absorb floods, per-key quotas at the gateway, and a few cost-based limits inside expensive services.</p>\n<figure class=\"diagram\" data-text=\"Clients -> Edge or CDN per-IP limit; Edge or CDN per-IP limit -> API gateway per-key limit; API gateway per-key limit <-> Redis counters (check); API gateway per-key limit -> Services cost limits; Services cost limits -> Database\"><div class=\"diagram-scroll\"><svg class=\"dg\" viewBox=\"0 0 800 179\" width=\"800\" role=\"img\" aria-label=\"Limits are layered. Cheap checks happen early, detailed ones later.\"><defs><marker id=\"dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path class=\"dg-arrowhead\" d=\"M0,1 L9,5 L0,9 z\"></path></marker></defs><line class=\"dg-edge\" x1=\"151\" y1=\"138\" x2=\"167\" y2=\"138\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"311\" y1=\"138\" x2=\"327\" y2=\"138\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"400\" y1=\"107\" x2=\"400\" y2=\"78\" marker-end=\"url(#dg-arrow)\" marker-start=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"471\" y1=\"138\" x2=\"487\" y2=\"138\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"631\" y1=\"138\" x2=\"647\" y2=\"138\" marker-end=\"url(#dg-arrow)\"></line><rect class=\"dg-node dg-client\" x=\"12\" y=\"114\" width=\"136\" height=\"48\" rx=\"24\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"142.5\">Clients</tspan></text><rect class=\"dg-node dg-box\" x=\"172\" y=\"112\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"240\" y=\"134.5\">Edge or CDN</tspan><tspan x=\"240\" y=\"150.5\">per-IP limit</tspan></text><rect class=\"dg-node dg-box\" x=\"332\" y=\"112\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"134.5\">API gateway</tspan><tspan x=\"400\" y=\"150.5\">per-key limit</tspan></text><path class=\"dg-node dg-store\" d=\"M332,18 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M332,18 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"41.5\">Redis</tspan><tspan x=\"400\" y=\"57.5\">counters</tspan></text><rect class=\"dg-node dg-box\" x=\"492\" y=\"112\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"560\" y=\"134.5\">Services</tspan><tspan x=\"560\" y=\"150.5\">cost limits</tspan></text><path class=\"dg-node dg-store\" d=\"M652,116 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M652,116 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"720\" y=\"145.5\">Database</tspan></text><rect class=\"dg-edge-bg\" x=\"408\" y=\"83.5\" width=\"41\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"428.5\" y=\"96.5\" text-anchor=\"middle\">check</text></svg></div><figcaption>Limits are layered. Cheap checks happen early, detailed ones later.</figcaption></figure>","headings":[{"id":"where-the-limiter-lives","title":"Where the limiter lives","depth":3}]},{"id":"choosing-the-key","title":"Choosing the key","html":"<h3 id=\"choosing-the-key\">Choosing the key</h3>\n<p>The limiter counts requests per key, and the key you choose decides who gets hurt by a false positive.</p>\n<ul>\n<li><strong>Per IP address.</strong> This works before authentication, so it's the only option for login pages, sign-up forms and anonymous endpoints. But many people can share one IP behind corporate proxies and carrier-grade NAT, so a strict per-IP limit blocks innocent users. Attackers with botnets have many IPs. With IPv6, a single host can rotate through a huge range of addresses, so limit by prefix (often the /64) rather than by full address.</li>\n<li><strong>Per user or account.</strong> This is fair and accurate for authenticated traffic, and it follows the user across devices. It doesn't help before login, and it doesn't stop one person creating many accounts.</li>\n<li><strong>Per API key or tenant.</strong> This maps directly to billing tiers (\"free: 60 requests a minute, pro: 1,000\"). One key may serve thousands of end users of a customer's app, so set the limit with that in mind.</li>\n<li><strong>Per endpoint or by cost.</strong> A search might cost 10 tokens and a profile read 1. That protects expensive operations without starving cheap ones.</li>\n<li><strong>Global.</strong> A ceiling on total traffic to a fragile backend, regardless of who sends it.</li>\n</ul>\n<p>Real rules combine these, and a request must pass all of them. A login endpoint, for example, might allow 5 failed attempts per username per 15 minutes (to stop password guessing against one account) and 100 per IP per hour (to slow credential stuffing across many accounts).</p>","headings":[{"id":"choosing-the-key","title":"Choosing the key","depth":3}]},{"id":"the-algorithms","title":"The algorithms","html":"<h3 id=\"the-algorithms\">The algorithms</h3>\n<p>The five common algorithms are easiest to understand as a progression. Each one fixes a weakness of the one before it, and the last two take a different view: instead of counting requests in a window, they model a rate directly.</p>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<div class=\"table-wrap\"><table><thead><tr><th>Algorithm</th><th>State per key</th><th>Bursts</th><th>Accuracy</th></tr></thead><tbody><tr><td>Fixed window counter</td><td>one counter</td><td>up to 2× the limit across a boundary</td><td>coarse</td></tr><tr><td>Sliding window log</td><td>one timestamp per request</td><td>none past the limit</td><td>exact</td></tr><tr><td>Sliding window counter</td><td>two counters</td><td>small overshoot possible</td><td>approximate</td></tr><tr><td>Token bucket</td><td>token count + last refill time</td><td>allows bursts up to bucket size</td><td>exact for its rule</td></tr><tr><td>Leaky bucket (queue)</td><td>a bounded queue</td><td>smoothed to a fixed output rate</td><td>exact; adds delay</td></tr></tbody></table></div>\n<h4 id=\"fixed-window-counter\">Fixed window counter</h4>\n<p>Keep one counter per key per window (per minute, say), increment it on each request and reject once it passes the limit. It's the cheapest option and easy to reason about. The flaw is at the boundary. With a limit of 100 a minute, a client can send 100 requests at 12:00:59 and 100 more at 12:01:00, which is 200 in about a second. Clients that were rejected also tend to retry together when the window resets, causing a spike.</p>\n<h4 id=\"sliding-window-log\">Sliding window log</h4>\n<p>Store a timestamp for every accepted request. On each new request, drop timestamps older than the window, count what's left and allow the request if the count is under the limit. It's exact, but memory grows with the limit: a limit of 10,000 an hour means storing up to 10,000 timestamps per key. It suits small limits where precision matters.</p>\n<h4 id=\"sliding-window-counter\">Sliding window counter</h4>\n<p>Keep fixed-window counters for the current and previous windows, and weight the previous one by how much of it still overlaps the sliding window:</p>\n<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span>estimate = current + previous × (1 − elapsed fraction of current window)</span></span></code></pre>\n<p>Say the limit is 50 a minute. The previous minute saw 48 requests, and 40 seconds into this minute there have been 31. Two-thirds of the window has elapsed, so a third of the previous minute still counts: 31 + 48 × ⅓ = 47. The request is allowed, and so are the next two; the one after that is rejected. This assumes the previous window's requests were spread evenly, so it can be slightly off. It costs two counters per key, and it removes most of the boundary burst. Cloudflare described using this approach and found the error small in practice.</p>\n<h4 id=\"token-bucket\">Token bucket</h4>\n<p>Each key has a bucket that holds up to <code>capacity</code> tokens and refills at <code>rate</code> tokens per second. A request takes one token, or more if it's expensive. No token, no service. A client that's been quiet can burst up to the full capacity, while the long-run average can't exceed the refill rate. Two knobs, burst and sustained rate, map well onto how people actually use APIs, which is why it's the most common choice for public APIs (Stripe describes its token-bucket limiters in the post under Further reading).</p>\n<p>You don't need a timer to refill. Store the time of the last update and add the tokens owed whenever a request arrives:</p>\n<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> time</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">class</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> TokenBucket</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> __init__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, rate: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">float</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, capacity: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">float</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.rate </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rate              </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># tokens added per second</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.capacity </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> capacity      </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># largest burst allowed</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.tokens </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> capacity        </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># start full</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.updated </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> time.monotonic()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> try_acquire</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, cost: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">float</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1.0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">float</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Return 0 if allowed, otherwise the seconds to wait before retrying.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> time.monotonic()</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Refill lazily from the elapsed time.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.tokens </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> min</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.capacity, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.tokens </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.updated) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.rate)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.updated </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> now</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.tokens </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">>=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cost:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.tokens </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cost</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0.0</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (cost </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.tokens) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">/</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.rate</span></span></code></pre>\n<p>The wait time it returns is exactly what you'd put in a <code>Retry-After</code> header. This version is single-process and not thread-safe; the distributed version comes later.</p>\n<h4 id=\"leaky-bucket\">Leaky bucket</h4>\n<p>There are two versions of the name. As a meter, it's the token bucket viewed from the other side: a bucket that fills with each request and drains at a fixed rate, rejecting when full. As a queue, requests wait in a fixed-size FIFO and are released at a constant rate, and new requests are dropped when the queue is full. The queue form smooths traffic into a steady stream, which protects a backend that can't handle bursts, at the price of added latency. NGINX's <code>limit_req</code> uses this method. Its <code>burst</code> setting sizes the queue, and <code>nodelay</code> passes queued requests through immediately, which makes it behave much like a token bucket.</p>\n<div class=\"widget-slot\" data-widget=\"rate-limiter\"></div>","headings":[{"id":"the-algorithms","title":"The algorithms","depth":3},{"id":"fixed-window-counter","title":"Fixed window counter","depth":4},{"id":"sliding-window-log","title":"Sliding window log","depth":4},{"id":"sliding-window-counter","title":"Sliding window counter","depth":4},{"id":"token-bucket","title":"Token bucket","depth":4},{"id":"leaky-bucket","title":"Leaky bucket","depth":4}]},{"id":"distributed-counters-in-redis","title":"Distributed counters in Redis","html":"<h3 id=\"distributed-counters-in-redis\">Distributed counters in Redis</h3>\n<p>With one gateway instance, the counters can live in memory. With twenty, they can't: each instance would see a twentieth of a client's traffic and enforce a limit twenty times too loose. The usual fix is a shared, fast store, most often Redis.</p>\n<p><strong>The race.</strong> The naive version reads the count, checks it and writes it back. Two gateways can both read 99, both decide the request is under the limit of 100, and both write 100. Both requests get through, and the counter is now wrong. Under real concurrency this happens constantly.</p>\n<p><strong>Atomic increments fix the simple case.</strong> <code>INCR</code> increments and returns the new value in one step, so check the result <em>after</em> incrementing: if <code>INCR</code> returns 101, reject. For a fixed window, put the window number in the key name (for example <code>rl:user42:28732150</code>) and send <code>INCR</code> and <code>EXPIRE</code> together in a <code>MULTI</code>/<code>EXEC</code> transaction so the key can't be left without an expiry if the client crashes between the two commands. Because the window is part of the name, refreshing the expiry on every request is harmless.</p>\n<p><strong>Lua scripts fix the rest.</strong> A token bucket or sliding log needs to read state, decide and write several fields. Redis runs a Lua script atomically: no other command runs while it executes. Here is a sliding-window log as one script:</p>\n<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">-- KEYS[1]: the limit key. ARGV: now_ms, window_ms, limit, unique request id</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">local</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> now    </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> tonumber</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ARGV[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">local</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> window </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> tonumber</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ARGV[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">local</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> limit  </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> tonumber</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ARGV[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">redis.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">call</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'ZREMRANGEBYSCORE'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, KEYS[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'-inf'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> window)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> redis.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">call</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'ZCARD'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, KEYS[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> limit </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">then</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  redis.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">call</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'ZADD'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, KEYS[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], now, ARGV[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">4</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  redis.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">call</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'PEXPIRE'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, KEYS[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], window)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">end</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span></code></pre>\n<p>The request ID must be unique, because a sorted set holds each member once, and two requests in the same millisecond with the same member would count as one. The time comes from the caller here, so gateway clocks need to agree. Alternatively, read Redis's own clock with <code>TIME</code> inside the script, which Redis 5 and later allow.</p>\n<p>Other things to plan for:</p>\n<ul>\n<li><strong>Latency.</strong> Every request now makes a round trip to Redis, around half a millisecond inside a data center. Keep Redis close to the gateways.</li>\n<li><strong>Sharding.</strong> In Redis Cluster, every key a script touches must live in the same hash slot. One key per limit avoids the problem; use hash tags like <code>{user42}</code> if you need several.</li>\n<li><strong>Hot keys.</strong> One huge tenant's counter lives on one Redis shard (see <a href=\"/learn/consistent-hashing\">consistent hashing</a>). If that's a problem, let each gateway take a batch of tokens locally and sync with Redis periodically. You accept a small overshoot in exchange for far fewer round trips. Envoy's design is similar in spirit: a local rate limit filter in each proxy, plus an optional call to a global rate limit service backed by Redis.</li>\n<li><strong>Read from the primary.</strong> Reading counts from a lagging replica undercounts.</li>\n<li><strong>When Redis is down.</strong> You must choose. <strong>Fail open</strong> (allow everything) keeps the product up but drops protection; many user-facing APIs do this, often falling back to a looser per-instance limit. <strong>Fail closed</strong> (reject everything) is right when each request costs real money or the limit is a security control, such as sending SMS codes.</li>\n</ul>","headings":[{"id":"distributed-counters-in-redis","title":"Distributed counters in Redis","depth":3}]},{"id":"telling-the-client","title":"Telling the client","html":"<h3 id=\"telling-the-client\">Telling the client</h3>\n<p><strong>Status code.</strong> Use <code>429 Too Many Requests</code>, defined in RFC 6585, when a client exceeds its limit. Some services use <code>503 Service Unavailable</code> when the whole system is overloaded rather than one client. The distinction helps clients: a 429 means \"you slow down\", a 503 means \"everyone wait\".</p>\n<p><strong>When to retry.</strong> <code>Retry-After</code> (defined in HTTP semantics, RFC 9110) gives either a number of seconds or a date. Seconds are safer, because they don't depend on the client's clock being right.</p>\n<p><strong>Remaining quota.</strong> There's no finished standard yet. Many APIs use unofficial <code>X-RateLimit-*</code> headers. GitHub, for example, sends <code>x-ratelimit-limit</code>, <code>x-ratelimit-remaining</code>, <code>x-ratelimit-used</code> and <code>x-ratelimit-reset</code> (a Unix timestamp). The IETF HTTP APIs working group is standardizing this in draft-ietf-httpapi-ratelimit-headers. Early versions of the draft defined separate <code>RateLimit-Limit</code>, <code>RateLimit-Remaining</code> and <code>RateLimit-Reset</code> fields. Recent versions use two structured fields: <code>RateLimit-Policy</code> describes the quota, and <code>RateLimit</code> reports what's left. Since it's still a draft, check the current version before copying the syntax.</p>\n<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">HTTP</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">/</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1.1</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 429</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> Too Many Requests</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">Retry-After</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> 7</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">RateLimit-Policy</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"per-key\";q=100;w=60</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">RateLimit</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"per-key\";r=0;t=7</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">Content-Type</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> application/json</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">{</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"rate_limited\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\"message\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Limit of 100 requests per minute exceeded.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span></code></pre>\n<p>A few habits make limits easier to live with:</p>\n<ul>\n<li><strong>Send quota headers on successful responses too,</strong> so well-behaved clients can pace themselves before they hit the wall.</li>\n<li><strong>Expect clients to back off with jitter,</strong> so they don't all retry at the same instant (see <a href=\"/learn/reliability-patterns\">reliability patterns</a>).</li>\n<li><strong>Roll out new limits in shadow mode first.</strong> Log what would have been rejected, check that real customers aren't caught, then enforce.</li>\n<li><strong>Consider alternatives to rejecting.</strong> You can queue and delay (the leaky bucket), serve a cached or reduced response, or limit concurrency instead of rate. Stripe, for example, describes a limiter on in-flight requests per user alongside its request-rate limiter, and separate load shedders that drop low-priority traffic first when the whole fleet is under pressure.</li>\n</ul>","headings":[{"id":"telling-the-client","title":"Telling the client","depth":3}]},{"id":"check-yourself","title":"Check yourself","html":"<h3 id=\"check-yourself\">Check yourself</h3>\n<details>\n<summary>A fixed-window limiter allows 100 requests per minute. What's the most a client can get through in any 2-second span, and which algorithm would prevent it?</summary>\n<p>Up to 200: 100 at the very end of one window and 100 at the very start of the next. A sliding window log prevents it exactly. A sliding window counter prevents most of it, at the cost of two counters. A token bucket with a capacity of 100 still allows a 100-request burst, but not 200, because the bucket can't refill instantly.</p>\n</details>\n<details>\n<summary>A token bucket refills at 10 tokens per second and holds 50. A client that's been idle sends 80 requests at once. How many succeed, and what Retry-After should the next rejected request get?</summary>\n<p>The bucket is full after the idle period, so 50 succeed and 30 are rejected. The bucket is then empty and refills one token every 0.1 seconds. <code>Retry-After</code> takes whole seconds, so you'd round up and send 1.</p>\n</details>\n<details>\n<summary>Two gateway instances share a Redis counter, using GET, then compare, then SET. What goes wrong under load, and how do you fix it?</summary>\n<p>Both instances can read the same value, both decide the request is allowed, and both write the same incremented value. Requests slip past the limit and the count is too low. Use <code>INCR</code> and check the value it returns, or move the whole read-decide-write into a Lua script so Redis runs it atomically.</p>\n</details>\n<details>\n<summary>Your rate-limit Redis cluster goes down. Should a public read API fail open or fail closed? What about an endpoint that sends SMS verification codes?</summary>\n<p>The read API should usually fail open, ideally falling back to a looser per-instance limit, since rejecting every request turns a limiter outage into a full outage. The SMS endpoint should fail closed: each request costs money, and attackers abuse unprotected SMS endpoints for toll fraud. The answer depends on whether the limit protects capacity or protects money and security.</p>\n</details>\n<details>\n<summary>A B2B customer complains that their users get 429s even though each user sends only a few requests. All their traffic comes from their own backend. What's likely happening?</summary>\n<p>They're probably being limited per IP or per API key, and their backend sends all their end users' traffic through one IP and one key. Every user shares a single quota. You could raise that customer's tier, offer per-end-user limits (letting them pass a user identifier you trust), or document the limit so they can queue on their side.</p>\n</details>\n<h3 id=\"further-reading\">Further reading</h3>\n<ul>\n<li><a href=\"https://stripe.com/blog/rate-limiters\" target=\"_blank\" rel=\"noopener noreferrer\">Stripe, \"Scaling your API with rate limiters\"</a>: four kinds of limiter and load shedder in production, with a token bucket in Redis.</li>\n<li><a href=\"https://blog.cloudflare.com/counting-things-a-lot-of-different-things/\" target=\"_blank\" rel=\"noopener noreferrer\">Cloudflare, \"How we built rate limiting capable of scaling to millions of domains\"</a>: the sliding window counter at very large scale, with an accuracy analysis.</li>\n<li><a href=\"https://www.rfc-editor.org/rfc/rfc6585\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 6585</a>: defines the 429 status code.</li>\n<li><a href=\"https://www.rfc-editor.org/rfc/rfc9110\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9110, HTTP Semantics</a>: includes <code>Retry-After</code> and <code>503</code>.</li>\n<li><a href=\"https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/\" target=\"_blank\" rel=\"noopener noreferrer\">IETF draft: RateLimit header fields for HTTP</a>: the proposed standard headers, with history of each version.</li>\n<li><a href=\"https://redis.io/docs/latest/commands/incr/\" target=\"_blank\" rel=\"noopener noreferrer\">Redis INCR documentation</a>: includes rate limiter patterns built on <code>INCR</code>.</li>\n<li><a href=\"https://nginx.org/en/docs/http/ngx_http_limit_req_module.html\" target=\"_blank\" rel=\"noopener noreferrer\">NGINX limit_req module</a>: a leaky bucket limiter with <code>burst</code> and <code>nodelay</code>.</li>\n<li><a href=\"https://sre.google/sre-book/handling-overload/\" target=\"_blank\" rel=\"noopener noreferrer\">Google SRE book, \"Handling Overload\"</a>: per-client quotas, client-side throttling and request criticality.</li>\n<li>Alex Xu, <em>System Design Interview – An Insider's Guide</em> (2020), \"Design a rate limiter\".</li>\n</ul>","headings":[{"id":"check-yourself","title":"Check yourself","depth":3},{"id":"further-reading","title":"Further reading","depth":3}]}],"sourceFiles":[]}