Your rate limiter runs in five regions with a cap of 100 requests a minute per user. One user gets 500 through. Where did the counting go wrong?
Global rate limiting fails when each region owns a local counter, because five regions each allowing 100 requests creates a 500-request ceiling. Use one atomic shared counter, route each user to an owning region, or lease regional budget deliberately. It also connects distributed counters to the point an interviewer is testing.
What the interviewer is scoring
- Does the candidate derive the 500 from the five local counters before proposing any fix
- Whether the shared counter is described as one atomic mutation rather than a read followed by a write
- That splitting the cap into five per-region budgets is recognised as a different failure and not a fix
- Can they say what the limiter should do when the shared store is unreachable, and defend the choice
- Whether the fixed-window boundary is raised without being prompted for it
Answer
Short answer
A global rate limit cannot be enforced by five independent regional counters. Either every region mutates one shared atomic counter, requests for a user are routed to that user's owning region, or each region leases part of the global budget from a coordinator. Static per-region splitting is not equivalent; it under-limits users who stay in one region and still needs a story for failover and borrowing.
Five tallies and no total
Each region counted to 100. Nobody counted to 500. That is the entire defect, and the arithmetic is the answer: five regions, a per-region counter, a cap of 100 in each, so the reachable ceiling for one user is 500 a minute. The limiter did exactly what it was built to do. It was built to enforce a local limit and asked to enforce a global one.
A nightclub with five doors and a clicker at each door has the same problem. Each doorman holds his door to a hundred. The room still fills to five hundred. The analogy breaks at the exit: a club's clicker can be decremented when somebody leaves, and a request rate has no leaving, only a window that expires. That difference is why the fix is about where the number lives rather than about counting more carefully.
Worth checking before you accept the premise: was traffic for this user spread across regions, or did it land in one? Anycast and latency-based DNS both move a client between regions on a network change, and a mobile client switching from cellular to wifi can change its egress region mid-session. If all five regions saw this user, the load balancing is not broken. It is working, and the limiter's assumption of locality was the wrong assumption.
The counter has to be one value, mutated atomically
The correct shape is a single counter per user per window, in a store every region can reach, incremented and tested in one operation. INCR returns the value after incrementing, which is what makes it usable here: one round trip gives you both the mutation and the decision. Read-then-write in two calls reintroduces the bug at a smaller scale, because two regions can both read 99 and both write 100.
# One atomic step per request: increment, then set the window's lifetime
# only on the call that created the key.
INCR ratelimit:{user-42}:2026-08-12T14:31
EXPIRE ratelimit:{user-42}:2026-08-12T14:31 60 NX
That is two operations, not one. The gap between them matters. If the process dies after INCR and before EXPIRE, the key has no lifetime and that user is capped for ever. Wrap both in one server-side script so they succeed or fail together, or use a data type whose expiry is set at creation. This is the kind of detail an interviewer is listening for, because it is the difference between having read about distributed counters and having operated one.
The cost is a cross-region round trip on every request. If your regions are far apart that cost is larger than the work being protected, and at that point the honest answer is not a faster counter. It is to stop making the decision globally.
Routing beats sharing
Give each user an owning region. Hash the user identifier, map the hash to a region, and route that user's requests there for limiting purposes. The counter is local again, and correct, because one region now sees all of that user's traffic by construction. You have converted a consistency problem into a placement problem. That is usually the better trade.
flowchart LR
C[Client] --> E[Edge in nearest region]
E -->|hash user id| O[Owning region for this user]
O --> L[Local counter for user]
L -->|under cap| S[Service]
L -->|over cap| R[429 with Retry-After]
O -->|owner unreachable| F[Local fallback cap]The edge in the diagram makes two decisions, and only the second is about the limit. Look at the fallback edge: it is where the design either degrades honestly or lies about the cap.
Splitting the cap five ways is a different bug, not a fix
The tempting cheap answer is 20 per region. It holds the global ceiling at 100 and breaks the product. A user whose traffic lands entirely in one region is cut off at 20 while 80 requests of headroom sit unused in four other regions. You have replaced over-permitting with under-permitting. The second is the one users file tickets about.
What makes static splitting workable is borrowing. Each region takes a lease on a slice of the budget from a coordinator, spends it locally with no round trips, and asks for more when it runs low. Requests are checked against local state, so the common path stays fast, and the coordinator only sees traffic at lease boundaries. You are trading precision for latency on purpose: a lease that has been handed out cannot be recalled instantly, so the global cap becomes approximate at the edges.
The window boundary doubles your cap on its own
Even with one perfect global counter, a fixed one-minute window permits 200 requests in a two-second span. The user spends 100 at 12:00:59 and 100 more at 12:01:00, and both are inside their own window. Nothing is broken and the limit was still exceeded, which is why "one user got more than the cap" needs the window shape established before you go hunting for a distributed-systems cause.
A sliding window over the last 60 seconds removes that, at the cost of keeping per-request timestamps or a set of finer-grained buckets. A token bucket removes it differently, by capping the burst at the bucket size rather than at a window total. Say which of the three you are choosing and what you accept in return.
What the limiter does when it cannot count
The shared store will be unreachable at some point, and the design has to have an answer that is not "throw". Failing open keeps the product working and removes the protection at exactly the moment traffic is strange. Failing closed protects the database and turns a cache outage into a full outage.
The usable middle is a local fallback with a tighter cap and a loud signal. Each region enforces its own conservative number from memory, emits a metric saying the limit is degraded, and the total exposure is bounded by that number times the region count. A strong candidate says the choice out loud: "This limit protects the payment provider, so I fail closed; the search limit protects nothing, so I fail open."
The question is never which algorithm to use. It is who owns the number, and whether every request that spends the budget can see the same copy of it.
© 2026 Preptima. Originally published at preptima.com.
Likely follow-ups
- If you route each user's requests to one owning region, what happens to a user whose owning region is being drained for a deploy?
- How would you let a region borrow unused budget from the other four without a round trip on every request?
- Does a 429 you returned count against the user's own limit, and does a client retry count twice?
- Where would you put the limit if the expensive thing is not the request count but the size of the response?
Related questions
- You need to enforce 100 requests per minute per API key across a fleet of forty servers. Which limiting algorithm, and where does the counter live?hardAlso on rate-limiting and token-bucket8 min
- One tenant bursts to ten times their normal traffic and every other customer's latency doubles. Your global rate limit was never hit. How would you design for fairness instead?hardAlso on rate-limiting and token-bucket5 min
- Design a rate limiter that allows N requests per client per minute. Write the class.mediumAlso on rate-limiting and token-bucket4 min
- Traffic is a hundred times normal and some of it is real customers. What do you drop first?hardAlso on load-shedding5 min