Distributed API rate limiter
Design a rate limiter for a public API served from many machines across several regions. Each customer has a plan with a request quota. The limiter must not become the reason the API is slow or down.
Where’s the slack?
Decision: ~1 ms. Accuracy: generous.No time slack at all, because the decision sits in front of every request. But a lot of accuracy slack, and the design is built on that trade.
There is no latency to spend. The limiter runs before the work it protects, on every request, so it must cost a fraction of what it saves. A limiter that adds 20 ms to protect a 30 ms endpoint has made things worse. A realistic budget is about a millisecond, which rules out anything that needs coordination between regions.
But the accuracy budget is wide, and seeing that is what makes the problem tractable. If a customer's limit is 1,000 requests a minute and they occasionally get 1,050 through, the backend barely notices. The limit exists to protect capacity and prevent abuse, and both goals survive a few percent of error, as long as capacity was planned with that margin.
That asymmetry is unusual. Most systems in this app trade latency for accuracy in small amounts; here you can trade a lot of accuracy for a little latency, and you should say so out loud.
The one place accuracy tightens is security limits: login attempts and password resets. There, being generous means letting an attacker have more guesses, so those get exact counting and a slower path, because they are rare enough to afford it.
So: A millisecond budget plus generous accuracy means local decisions with asynchronous reconciliation instead of a globally consistent counter. It also means capacity limits must fail open, because a limiter outage must not become an API outage.
Work it through
Five stages, gated. You write your answer for each stage before its model answer unlocks. That is the core of the method.
Reference material for this problem. The diagrams below are here to check your own against once you have drawn it, not to read first.
The lifecycle
The numbers
A cross-region hop is 150x the budget, so global consistency on the request path is ruled out. So the hard part is deciding without coordinating: trade accuracy, which you have plenty of, for latency, which you have none of.
The architecture
The mechanism
The decision this turns on
- —Zero network calls on the request path, so comfortably inside 1 ms.
- —Survives the counter store being down: degrades to per-server limits.
- —Over-admission bounded by servers × sync interval, and you can state the bound.
- —A customer spreading across regions gets somewhat more than their quota.
- —Accurate within a region, and easy to explain to a customer.
- —Adds a network hop to the most-executed code path in the system.
- —The store becomes a hard dependency of every request.
- —Still cannot be globally accurate: a cross-region hop is 150x the budget.
Follow-ups you should expect
Many interviews are decided in the follow-ups, not the first diagram.
Same shape, other products
Deciding whether to say no, cheaply, on every request.
The decision has to be faster than the work it protects, which rules out most of the accurate answers.
Others with this shape: API rate limiter · Distributed counter · Fraud checks
How it is really built
Write your own answer first. Then read how teams who run this at scale describe it. Their constraints were real, and the write-ups often say what their choices cost.
- Using load shedding to avoid overload ↗The Amazon Builders' Library on rejecting work early, which is what a limiter does.
- Google SRE Book — Handling Overload ↗Graceful degradation when the limit is reached.