System Design Concepts · API Design
Rate limiting shares capacity fairly, and the algorithm decides how
A limiter protects a shared resource from any one caller, malicious or just retrying badly. The algorithm decides which bursts get through.
A fixed window lets two bursts in
- A limiter allows 100 requests per clock minute, and at 0:00 its counter is at 0, so the limiter lets requests through. The timeline runs from 0:00 to 2:00, and the first minute's window ends at 1:00.
- Three requests arrive in the first 40 seconds, and the counter goes 1, 2, 3 by 0:40. The limiter lets all three through, because a few spread-out requests never reach the limit.
- At 0:59, 97 requests arrive in one second, just before the minute boundary. The counter reaches 100 of 100, so this minute's window is full, but the limiter has not refused a single request.
- A new minute starts at 1:00, so the counter resets from 100 to 0. A fixed window forgets every request from before the boundary, even the burst from one second ago.
- At 1:01, 100 more requests arrive just after the boundary, and the limiter passes every one as the counter reaches 100 again. So 197 requests get through in 2 seconds under a limit of 100 per minute, exactly what a retry storm (a surge of retried requests) produces.
- A sliding window at 1:01 covers the last 60 seconds, which still include 97 requests from 0:59 and 3 earlier ones. The count is 100, so the limiter refuses the same second burst with 429. The cost is 100 timestamps per caller, not one integer, so the usual compromise uses a weighted count from the previous window.
A token bucket pays for the burst
- A gateway between the client and the API keeps a token bucket that holds up to 20 tokens and refills 2 tokens per second. The bucket starts full at 20 of 20 tokens, and a request gets through the gateway only by taking a token.
- The client sends a burst of 20 requests at once, and each request takes a token, so the bucket drops from 20 tokens to 0. The API serves all 20, because the bucket allows a burst this size on purpose.
- The bucket has no tokens left, so the gateway refuses one more request and sends the client a 429 (Too Many Requests). The bucket still holds 0 of 20 tokens.
- Tokens come back at 2 per second, and 4 requests arrive evenly spaced at that pace, so the served count goes from 20 to 24. Each request takes a token as it arrives, so the bucket never holds more than 1. Meanwhile 5 tokens come back, one more than the requests, so the bucket ends at 1 of 20.
- No requests arrive while 3 more tokens come back, so the bucket holds 4. Then 4 requests arrive in one clump, empty the bucket to 0 and take the served count to 28. Next, 4 more tokens come back. Spaced or clumped, the requests average 2 per second. A leaky bucket queues requests, so it would smooth the clump instead.
- The bucket is full again, and the API now runs as 3 instances. A bucket in each instance would allow a burst of 60, 3 times the limit, so there is one shared bucket in the gateway's Redis. Then 3 requests take one token each, from 20 down to 17, and go on to the 3 instances.
Tell the client how to back off
- The server answers Client A's request with a 200 reply that carries three headers: X-RateLimit-Limit 100, X-RateLimit-Remaining 1 and X-RateLimit-Reset 30 s. With these de facto headers (the draft standard names them RateLimit-*), a client can read its request budget and stay inside it.
- Both clients send again with no budget left, so the server answers each with a 429 carrying Remaining 0 and Retry-After: 30 (seconds or an HTTP date). A 429 means this one caller is over its limit, but a 503 would mean the whole service is down.
- Client A ignores Retry-After and retries at once, 5 times in two seconds, all at 0 s on the retries timeline. The server refuses every retry with a 429, so its refused count rises to 7. Retrying instantly is how one slow minute becomes an outage.
- Client B follows the Retry-After header and waits 30 seconds, until its countdown reaches 0. Then Client B sends one request at 30 s on the retries timeline, and the server answers 200 with Remaining 99 and Reset 60 s.
- A group of 50 clients got the same Retry-After: 30, so all of them retry in the same second. The server gets 50 requests in 1 s, at 30 s on the retries timeline, so polite clients that retry together are a burst.
- Jitter adds a random delay, so each client waits 30 seconds plus a random part of 30 more. The same 50 retries now spread from 30 s to 60 s, so the server gets the 50 requests over 30 s. Exponential backoff doubles the wait after each failure, but without jitter the clients retry together on every round.
© LearnThatStack - diagrams may not be republished without permission.
A limit exists for four reasons: it stops abuse and holds back a client stuck in a retry loop. The limit also caps what one caller can cost you and keeps one tenant from taking the capacity the others need. This limiter allows 100 requests per clock minute, and at 0:00 its counter starts at 0. The limit is a promise about a shared resource, not a punishment.
Most traffic looks like this: a client sends three requests in the first 40 seconds. The limiter counts them as 1, 2 and 3 and lets each one through. A client that behaves never reaches the limit at all. For that client, the counter is bookkeeping that the limiter never acts on.
The limiter refuses no request here, because the window is the budget for this minute and this burst spends that budget. A burst of 97 requests arrives at 0:59, the last second of the minute. The counter reaches 100 of 100, so this minute's window is now full. But the minute is about to end.
The reset at the window boundary is the whole flaw of a fixed window. At 1:00 the limiter sets its request counter from 100 back to 0. So the limiter keeps no record of the old window, not even the 97 requests from one second ago. A retry storm (a surge of retried requests) is exactly the traffic that triggers this flaw.
Counting requests per clock window leaves a loophole at the window boundary. A second burst of 100 requests arrives at 1:01, just after the boundary. The new window's counter reaches 100 again. So 197 requests pass in two seconds, under a limit of 100 per minute. The total is nearly twice the limit. But the limiter never refused a single request.
A sliding window closes the loophole and refuses the same second burst with 429. At 1:01, the last 60 seconds still include the 97 requests from 0:59 and the 3 from earlier, so the counter stays at 100. This fix is accurate but costs memory, because each caller needs a log of 100 timestamps instead of one integer. Most limiters use a compromise: a sliding counter that weights the previous window's count.
The fixed window reset its count at every minute boundary. A token bucket remembers the tokens already spent, and it allows a burst on purpose. Here the gateway's bucket has a capacity of 20 tokens and refills at 2 tokens per second. Capacity and refill are two product decisions: capacity sets the burst allowed at once, and refill sets the rate allowed over time.
The limiter allows a burst of 20 on purpose. A page load is a burst of requests, so a limiter that refused that burst would block normal use. Each token in the bucket lets one request through. Twenty requests take one token each, so the API serves all twenty and the bucket drops from 20 to 0.
The limit exists for exactly this request, the 21st. The capacity is 20 tokens, one per request, so a 21st request in the same instant has no token to take. The gateway refuses that request and sends a 429 back to the client. The client has to wait until a token is available again.
The refill rate is the long-run limit, and the limiter enforces it one token at a time. The limiter adds back 2 tokens per second, and four requests arrive at that same pace. Each request uses one token as it arrives, so the bucket of unused tokens never holds more than 1. The limiter never refuses a caller who stays at this rate.
The caller gets the same budget, bursty or smooth: an average of 2 requests per second. The bucket gains three tokens while no requests arrive, so it holds 4. A burst of four requests then uses all four tokens, one token per request, so the API serves every one. Four more tokens later refill the bucket to 4. A leaky bucket instead queues requests and passes them on at a fixed rate.
The API's three instances have to share one bucket, because separate buckets would allow 60 requests per burst, three times the limit. The gateway's Redis holds that one bucket, so three requests to three instances take three tokens from the same bucket. Per key, per IP and per endpoint limits are different buckets, and a global limit is another. You usually need more than one.
The limiter protects you, but the other half of rate limiting keeps your callers well behaved. Client A gets a 200 response with X-RateLimit-Limit 100, X-RateLimit-Remaining 1 and X-RateLimit-Reset 30 seconds. A client that can see its budget can stay inside it: Client A knows it has one request left until the reset in 30 seconds. The draft standard's RateLimit headers carry the same three numbers.
This refusal is a helpful error, because it tells each client when to come back. Both clients get a 429 Too Many Requests with Retry-After: 30, so each one knows to wait 30 seconds. The Retry-After value can be a number of seconds or an HTTP date. A 429 means this one caller is over its limit, but a 503 would mean the whole service is down.
Every instant retry costs the server work and achieves nothing. Client A ignores Retry-After, the header that says when to try again, and sends five retries in two seconds. The server answers all five with a 429 right away, so its refused count reaches 7. Retrying instantly is how one slow minute turns into an outage.
Client B shows the client half of rate limiting: read the header, wait, then succeed. Client B obeys Retry-After and waits the full 30 seconds. Then Client B sends just one request. That request gets a 200 and a fresh budget of 99. This loop is the half of rate limiting that interviewers ask about.
Retry-After tells each client how long to wait, so clients that all obey the same value come back at the same moment. Fifty clients got Retry-After: 30, so all fifty retry in the same second. The server receives 50 requests in 1 s. Fifty polite clients with the same wait are a burst, and the limiter was built to refuse a burst.
The fix is to make each client wait a different time. Jitter adds a random extra wait, so the server receives the same fifty retries spread over 30 seconds, from second 30 to second 60. Exponential backoff doubles every client's wait after each failure, but it doubles all the waits together. So without jitter, the clients all retry together again in every round.
I keep a token bucket of 20 in Redis, so all three instances share one count. The 20 is the burst I allow on purpose, and the refill of 2 per second is the rate. My 429 carries Retry-After, so a good client waits. My clients also back off exponentially with jitter, so they never retry at the same moment.
In interviews · 5 questions
Related Questions
- 01
- 02