deep dive 1 The first decisions
Before any box moves, three things have to be pinned, and they are the three the day-one server hand-waved: how we tell one client from another, how we count its requests, and what we send back when it is over. None of them need a new machine — all three are the shape of the check itself. Start with the client. The limit is per-client, so everything depends on how a client is identified: this is the limiter key, the value we count against. The obvious key is the caller's IP address — always present, needs nothing — but an IP is not a caller. A whole office, a whole carrier, a whole cloud region can sit behind one address. The opposite key is the authenticated identity — a user id or API key — which names an actual account, so the limit lands exactly where it should.
The day-one server used a plain counter that resets every minute. That is a real algorithm — a fixed window — and it has a specific, well-known flaw. Because the window snaps back to zero at a fixed instant, a client can send its whole allowance in the last second of one window and its whole allowance again in the first second of the next: two windows a fraction of a second apart, so the client just got about twice its limit in an eyeblink. That factor of two is the flaw every better algorithm exists to fix.
The exact fix is a sliding window log: store the timestamp of every request, and on each new request drop the ones older than the window and count what is left. Perfectly accurate — the window really slides, so there is no boundary to game — but the cost is memory. You store one entry per request, per client, even for requests you rejected. Figma measured this and found the log costs about ten times the memory of the cheaper approach. Ten times the memory to remove a flaw that lets someone send double for one second is a bad trade, which is why nobody ships the exact log at scale.
That leaves two that differ in one property: whether a client may burst. A leaky bucket — what nginx does — smooths the outflow to a fixed, steady rate and never lets a burst through. A token bucket does the opposite where it matters: a bucket holds some tokens and refills at a steady drip; each request takes one, and a client that has been quiet can spend a burst all at once, up to the bucket's size, then is held to the drip rate. The acceptable alternate is Figma's sliding-window counter, the approximate cousin of the log — chop the window into slices, keep a count per slice, add up the trailing slices — which you pick if you want smooth enforcement without a burst allowance and can accept up to a minute of leniency at the edge.
Token bucket — allow a burst up to a size, then enforce the average. Token bucket. The deciding reason is that real traffic is bursty and mostly fine — a user opens an app and it makes twelve calls at once; a nightly job wakes up and syncs. Punishing those with a leaky bucket's flat rate makes the product feel broken for behaviour that was never abusive. The token bucket allows the burst up to a set size and enforces the average after it, which is what almost every client actually needs. It is also the answer two independent production systems reached: Stripe runs its rate limiter on the token bucket, and AWS API Gateway does too, exposing exactly two dials — a rate, which is the refill speed, and a burst, which is the bucket size.
And what we say when a client is over. A rejection has to be an answer the client's code can act on, or the client just retries in a tight loop and makes everything worse. The status code is 429 Too Many Requests, and it may carry a Retry-After header saying how many seconds to wait before trying again. Those two things are the whole ratified contract, and 429 responses are explicitly not to be cached — a cached over-the-limit would keep rejecting a client that is now under it. You will also see extra headers like X-RateLimit-Remaining, and they are genuinely useful, but they are a convention several companies invented separately, not a web standard — so send them, but do not call them a standard.
Under the hood — how it works
The token bucket does not store a running countdown something has to tick down every moment — that would need a timer per client, which is millions of timers. It stores two numbers: how many tokens were in the bucket the last time this client was seen, and when that was. On the next request it computes how many tokens should have dripped in since then — elapsed time times the refill rate, capped at the bucket size — adds them, and checks for a token to spend. The refill is lazy, computed on read, so a client that goes quiet for an hour costs nothing until it comes back.
This matters later: a client's whole state is a couple of numbers, about fifty bytes, which is why the memory sizing a few use cases from now comes out so small.
The limiter-key rule, stated once: use the API key or user id when the request carries one, and fall back to the IP only for anonymous traffic, on a tighter limit — precisely because you cannot tell who is behind it. Once you know who is calling you can trust them with far more, because a bad key hurts only its own budget, while a bad IP might be hiding a thousand good callers behind it.
Stripe documents its rate limiter as a token bucket, and AWS API Gateway exposes the same two dials — a steady-state rate and a burst — which is the token bucket by another name. It is worth killing one myth: Stripe's engineering blog does not describe GCRA or redis-cell; it documents token bucket. Do not attribute an algorithm to a company that did not publish it.
Stripe — Scaling your API with rate limiters ↗