Distributed systems · 10 min
Token bucket
A rate limit with two knobs: how fast permits come back, and how many can pile up. Set both and a real app’s burst gets through while a flood does not.
Your public API promises each user 5 requests a second. Then your own mobile app ships a new home screen. When it opens, it fires 10 calls in the same instant: profile, feed, settings, notifications and six more.
Half of them come back with an error, and the home screen is blank. Nobody misbehaved. The app made 10 calls once and then went quiet. Yet a script hammering you in a loop also has to be stopped. How does one limit tell those two apart?
The wrong way first: count per clock second
This is the wrong way, but it is the first one everybody writes. Keep a counter per user. Allow the call if the counter is under 5, and reset the counter to 0 whenever the clock ticks over to a new whole second. This is called a fixed window.
It fails the app: 10 calls at once means 5 allowed and 5 refused, every time the app opens. And it fails the other way too.
Guess first: A fixed window allows 5 per clock second. A caller sends 5 requests at 0.95 s and 5 more at 1.00 s. How many get through?
All 10. The first 5 fit in the 0–1 s window. At 1.00 s the counter resets, so the next 5 fit in the 1–2 s window. That is 10 requests in 0.05 seconds from a limit that says 5 a second. A fixed window can let through up to twice its limit around every edge.
So the fixed window is too strict for the honest app and too loose for anyone who times their requests around the edge. What we want is one rule that forgives a short burst but still holds everyone to 5 a second over time.
A bucket of permits
Give each user a bucket of tokens. A token is a permit for one request. Each request takes one token out. No token left means 429.
Tokens drip back in at a steady speed, but the bucket has a size, and tokens that arrive when it is full spill over and are lost. That gives you exactly two knobs:
- Refill rate, r: how many tokens come back per second. This is the long-run speed limit.
- Bucket size, b: the most tokens the bucket can hold. This is the biggest burst you forgive. The demo calls it Capacity.
Walk the app through a bucket with b = 5 and r = 5. It starts full, with 5 tokens. The 10 calls arrive at once: the first 5 take the 5 tokens, and the other 5 find it empty and get 429. A tenth of a second later, 0.1 × 5 = 0.5 tokens have dripped back: still not enough for one request.
Yes, half a token. Tokens drip in smoothly rather than one at a time, so the bucket can hold a fraction. A request needs one whole token, and a refused request costs nothing.
The server does not need a timer ticking for every user. It stores just two numbers per user: tokens left, and the time it last looked. When a request arrives, it works out how much dripped in since then (seconds passed × r), adds it, caps the total at b, and then decides.
Turn the knobs and watch the burst
- Press Clear, then Burst of 10. With the sliders at b = 5 and r = 5, the Token bucket lane shows 5 green dots and 5 red ones: the app’s problem. The steps under the timeline explain the last red one: 429, with Retry-After: 0.20 s. That is a reply header telling the client when to try again: the time for one token to drip back at 5 a second.
- Drag Capacity down to 1. Only 1 of the 10 is green. Now drag it to 10 or more: all 10 go green. The Fixed window lane still refuses 5, because it only knows “5 per second”.
- Set both sliders back to 5 and press Edge-burst example. The token bucket allows 5 and refuses the 5 at 1.00 s. The fixed window lets all 10 through. The chart below counts how many were allowed in any 0.2 s stretch: 10 for the fixed window, 5 for the bucket.
- Press Clear, then Steady 3/s, then Play. It sends one request every third of a second (0, 0.33, 0.67, 1.00 s…), and everything stays green: 3 a second is under r = 5. When it finishes, drag Refill to 1. The first 7 requests still pass: the 5 stored tokens plus 2 that drip in over the first 2 seconds. From 2.33 s on, two in every three are refused.
Turn the bucket size all the way down
With b = 1, the bucket holds a single token. One request passes, and any request in the next 1/r seconds (0.2 s at r = 5) is refused, not queued. Only perfectly evenly spaced callers get everything through.
That sounds tidy, but real clients don’t send evenly spaced requests. You saw the app get 1 success and 9 refusals. A web page that fires four API calls at once to fill its widgets breaks the same way. A tiny bucket refuses every burst, even from callers who stay well under 5 a second on average.
Turn it all the way up
Now make b = 1,000. Your app is happy: its 10 calls barely dent the bucket. But a user who has been quiet for a few minutes now has 1,000 tokens saved up, and a script can spend all of them in the same second.
Your servers sized for “5 a second per user” just got 1,000 requests from one caller at once. A huge bucket does nothing against a short flood. It only starts refusing once all 1,000 are spent, and by then the damage is done.
Guess first: Bucket b = 5, r = 5. The app sends its 10 calls, then sits idle for 2 full seconds, then sends 10 more. How many of the second 10 get through?
Only 5. In 2 seconds, 2 × 5 = 10 tokens drip back, but the bucket holds at most 5, so the other 5 spill over. Waiting longer never saves up more than b. That cap is exactly what stops the b = 1,000 flood from growing even bigger.
The refill rate is the promise
The other knob is simpler. However big the bucket, tokens only come back at r a second, so over a long run nobody gets more than r a second. That is why, at r = 1, the steady caller got only 1 request in 3 once its stored tokens ran out.
Put the two knobs together and you can say exactly the most a user can get through. Take b = 10 and r = 5. In any 1-second stretch, starting at any moment, not just on the clock’s whole seconds: at most the 10 already in the bucket plus 5 that drip in, so 15. In any 60 seconds: 10 + 5 × 60 = 310, barely above the 300 that “5 a second” promises.
In general, in any T seconds a user gets at most b + r × T requests. Over short stretches the b part dominates: that is the burst. Over long ones the r × T part dominates: that is the rate. The fixed window has no such cap on short stretches, which is why it allowed 10 in 0.05 s.
Where to set the dial
| Setting | App opens (10 calls at once) | Script spending a full bucket at once |
|---|---|---|
| b = 1, r = 5 | 1 passes, 9 refused | 1 request |
| b = 5, r = 5 | 5 pass, 5 refused | 5 requests |
| b = 15, r = 5 | all 10 pass | 15 requests |
| b = 1,000, r = 5 | all 10 pass | 1,000 requests |
Set r from what you can afford: the steady load your servers can carry per user, or what the customer’s plan pays for. Set b from the biggest burst an honest client really makes, with a little room above it. Your app opens with 10 calls, so b = 15 lets it through with room to spare, and a script still can’t get more than 15 in one go.
Then sanity-check the busiest single second: b + r requests, 20 here. If one user sending that many at once would hurt your servers, the bucket is too big.
The whole limiter in a few lines
Look for the refill line: it is the only clever part, and it runs only when a request arrives.
b = 15 # bucket size: biggest burst
r = 5 # refill: tokens per second
buckets = {} # user -> (tokens, last_seen)
allow(user, now):
tokens, last = buckets.get(user, (b, now))
tokens = min(b, tokens + r * (now - last)) # drip in, cap at b
if tokens >= 1:
buckets[user] = (tokens - 1, now)
return ALLOW
buckets[user] = (tokens, now)
return REFUSE(429, retry_after = (1 - tokens) / r)The reply tells the client when to try again in a Retry-After header, so a well-behaved app waits instead of retrying straight away. If 5 refused calls all retry at exactly 0.20 s, only 1 token is there, so good clients also add a small random delay to spread them out.
Remember
- Refill rate r is the long-run limit; bucket size b is the biggest burst you forgive.
- In any T seconds a user gets at most b + r × T requests.
- A bucket of 1 refuses every honest burst; a huge bucket lets a flood through after a quiet spell.
- Set b just above the biggest real burst your clients make.
- A fixed window per clock second can let twice its limit through around an edge.
See it in a design
- Rate limiter: Token buckets in Redis checked at the gateway, so no single client can overwhelm the API.
- Flash sale: Limited drop that sells from an atomic Redis counter and writes orders through a queue.
- Notification system: One API for push, email and SMS with preferences, per-channel queues and retries.
Further reading and videos
- Rate Limiting | System Design Interview Basics (ByteMonk)
- How Rate Limiting Actually Works — Token Buckets, Sliding Windows, and Leaky Buckets (State & Flow)
- API Rate Limiting Algorithms | Token Bucket, Leaky Bucket, Fixed Window, Sliding Window (Code with GD)
- Design A Scalable Rate Limiter | System Design (Irtiza Hafiz)
- Scaling your API with rate limiters (Stripe)
- How we built rate limiting capable of scaling to millions of domains (Cloudflare)
- Rate Limiting with NGINX (NGINX)
- Build 5 Rate Limiters with Redis: Algorithm Comparison Guide (Redis)
- 429 Too Many Requests (MDN)
- Token bucket (Wikipedia)