Skip to main content

Distributed Rate Limiting

Level 4: Level 4: Scaling Compute & Traffichard30 minrate-limitingdistributedredis

Naive per-node limits grant Nx; enforce exactly with atomic Redis ops or hybrid local-cache-plus-async-sync for bounded overshoot, always with a fail-open plan.

One global limit across N nodes

A single-node limiter is easy because one process owns the counter. Production limiters run on a fleet of gateway nodes behind a load balancer, and a user's requests land on any of them. If each of 20 nodes independently enforces "100 req/min," a user spraying across all 20 gets 2000 req/min, 20x the intended limit. The whole problem of distributed rate limiting is enforcing one global limit across N nodes without paying an unacceptable latency or availability tax.

Check yourself
The fleet moves the counter to shared Redis. Each gateway node runs 'GET count', checks it against the limit of 100, and calls 'INCR' if allowed. Two nodes execute this at the same instant when the count is 99. What happens?

Approach 1: centralized exact, with a shared store. Put the counter in Redis and have every node read-modify-write it. The trap is the race: two nodes doing GET count; if under limit INCR can both read 99 and both allow, overshooting. You must make the decision atomic. For simple counters, INCR returns the new value in one round trip, so the node increments first and rejects if the result exceeds the limit. For token bucket you need multiple fields updated together (refill then decrement), so you run a Lua script via EVAL, which Redis executes atomically. This gives exact global enforcement. The cost is a network round trip to Redis on every request (0.5 to 1ms added per call) and Redis becoming a hot dependency.

Approach 2: local approximation. Give each node a slice of the budget: 2000/min total across 20 nodes means 100/min per node, enforced purely in local memory with zero coordination. Fast, no shared dependency, but only correct when traffic is evenly balanced. If the load balancer sends a hot user disproportionately to a few nodes, those nodes throttle early while the global budget is underused. It also wastes budget: idle nodes' slices are unusable by busy nodes.

Approach 3: hybrid, the common production answer. Nodes enforce locally from a local token cache for speed, and asynchronously sync their consumption to Redis every short interval (say 100ms) to true up the shared view and re-divide the remaining global budget. This bounds overshoot to at most one sync interval's worth of traffic while keeping the hot path in local memory. Envoy's global rate limiting and many CDNs work roughly this way.

Interview nuance: you will always be asked "what if Redis is down?" A rate limiter must not become a single point of failure for the whole API. The standard answer is fail open: if the shared store is unreachable, fall back to permissive local limits and let downstream load shedding protect the backend. Also handle hot keys (one celebrity key hammering a single Redis slot) with key sharding or local caching, and clock skew / window alignment across nodes: use the store's time or logical windows, not each node's wall clock.

naive per-node (BROKEN)          hybrid (production)
 node1: 100/min                   local cache enforces fast
 node2: 100/min   x20 nodes       async sync -> Redis every 100ms
 ...              = 2000/min       true up + re-divide budget
 user sprays -> 20x limit         overshoot bounded to ~1 interval

Recap: naive per-node limits grant Nx, so either enforce exactly via atomic Redis ops (INCR / Lua, paying a per-request round trip) or approximate locally and async-sync to a shared store for bounded overshoot, and always decide the fail-open path plus hot-key sharding so the limiter never becomes the outage.

Check yourself

Match each property to the distributed enforcement approach it describes.

Exact global enforcement, but every request pays a network round trip
Zero coordination, but a hot user concentrated on a few nodes gets throttled while global budget sits unused
Overshoot bounded to roughly one sync interval of traffic
How Envoy global rate limiting and many CDNs actually work
Only correct while the load balancer spreads traffic evenly

Apply

Your turn

The task this lesson builds to.

Extend a single-node rate limiter to a fleet of 20 gateway nodes without letting a user get 20x their limit.

Think about

  1. How do you keep the shared counter atomic under races?
  2. What is the tradeoff of local approximation vs centralized exactness?
  3. What happens if the shared store (Redis) is down?

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Design global rate limiting for a CDN edge with 200 points of presence across the globe where the per-customer limit must hold worldwide but a synchronous call to a central store on every request would add unacceptable latency. Justify your consistency choice.

Think about

  1. Why is strict centralized exactness off the table by requirement here?
  2. How do budget leases follow live demand across regions?
  3. What is the honest bound on overshoot, and why is it acceptable?