The window boundary burst
Your limit is 100 requests a minute, counted per clock minute. A client sends 100 at 0:59.9 and 100 more at 1:00.1. How many get through?
In 30 seconds
- The problem
- The limit is 100 a minute and 200 requests get through in 0.2 seconds.
- Why it happens
- The counter resets at the top of every minute. The first burst fills minute 0's counter, the second lands in a brand-new counter for minute 1.
- The fix
- Use a token bucket. It refills continuously, so there is no clock edge to time requests around.
- In an interview
- A fixed window limits each clock minute, not any 60 seconds. If a burst at the edge matters, use a token bucket or a sliding window, and say the trade-off.
Why it matters
This one has no race. The count is atomic and every number is correct. The flaw is the window itself, so code review and load tests both pass.
In our run, with the clock pinned, a limit of 100 a minute let 200 requests through in 0.2 seconds: 100 at 0:59.9 and 100 at 1:00.1. The token bucket let the first 100 through and none of the second burst.
Words you need
- Rate limit
- A cap on how many requests one user can make in a period. Past it, the API answers 429 Too Many Requests.
- Fixed window
- Count requests in each clock minute and reset at the edge. Simple, but a burst that straddles the edge can pass twice the limit.
- Sliding window
- Count the requests in the last 60 seconds, measured back from now, so there is no edge to exploit. Exact versions store every timestamp; cheaper ones blend two windows.
- Token bucket
- A bucket holds up to N tokens and refills at a steady rate. Each request takes one, and an empty bucket means reject. It allows short bursts and holds the average.
- Atomic
- Done as one step that nothing can interrupt. Another request sees it either not started or fully finished.
Picture it
The limit is 100 a minute. A client times two bursts of 100 either side of 1:00.
Fixed window
- 0:59.9: 100 requests
- Minute 0's counter reaches 100
- 1:00.1: a new counter starts at 0
- 100 more are allowed
200 requests in 0.2 seconds, and each minute looks within its limit.
Token bucket
- 0:59.9: 100 requests empty the bucket
- 0.2s later about 0.3 tokens have dripped back
- 1:00.1: the bucket is empty
100 get through. The second burst gets 429s.
A normal client
- 100 requests spread evenly over the minute
Both versions allow all 100. The fix does not punish ordinary traffic.
The fixes people try
Fixed window
FailsINCR rate:{user}:{minute}Bounds each clock minute, not any 60 seconds. A burst that straddles the edge gets twice the limit.
Smaller windows
Half a fix100 per minute → 2 per secondShrinks the edge burst but blocks normal bursts too, like a page load that fires 20 requests at once.
Sliding window counter
Worksweight last minute + this minuteTwo counters and a weighted sum. Close to exact, and cheap.
Sliding window log
WorksZADD every timestamp, count the last 60sExact, but stores one entry per request.
Token bucket
WorksEVAL take-tokenRefills continuously, so no edge exists. Allows a burst up to the bucket size, then the steady rate. Stripe's choice.
The fix and its cost
A token bucket holds up to 100 tokens and refills at 100 a minute, a little at a time. Each request takes one. The first burst empties it; 0.2 seconds later only about a third of a token is back, so the second burst is rejected.
Refill and take happen in one Lua script, so concurrent requests can never spend the same token.
What it costs
Interviewers also listen for: why Stripe picked it
Check yourself
1Why does this pass code review?Show answerHide
Nothing is wrong with the counting. Every number is correct; the window resets at the edge exactly as written.
2What is the worst case for a fixed window?Show answerHide
Twice the limit, sent as one burst just before the edge and one just after.
3Does the token bucket block normal users?Show answerHide
No. A client spreading 100 requests over a minute gets all 100, because tokens refill as fast as it spends them.
The code
limiter.bug.ts
Example code. The tested version, with a proof test against real Postgres, lands in the repo.
Star the repo for the runnable version
Each trap in this world ships with the bug, the fix and a proof test that fires concurrent requests at a real database.
Remember
A fixed window counts clock minutes. Bursts can live on the edge between two of them.