SysDesignPrep.com
Interactive concepts

Distributed rate limiting: local limits vs a central Redis counter, simulated

A rate limit is easy on one server: keep a counter. With a fleet of API gateways behind a load balancer, each sees only part of a tenant’s traffic, and the limit belongs to the tenant as a whole. Distributed rate limiting is the choice of where the count lives: split across the gateways, in one shared store such as Redis, or kept locally and shared now and then.

The playground limits one tenant to 100 requests a second across several gateways and runs the same traffic through all three designs. Try uneven load balancing, bursts and different sync intervals.

TrafficGatewaysSync every

One tenant may make 100 requests a second in total. It sends 300 requests a second, spread evenly, across 4 gateways. Local limits give each gateway 25 a second, which works here because the load balancer spreads requests evenly. The central counter is exact, at the price of a Redis call on every request, 313 a second, each adding about 1 ms. Syncing every 100 ms costs 40 Redis calls a second, but between syncs each gateway only knows its own count, so 226 requests beyond the limit get through.

Local limitseach gateway enforces its share of the limit
0200400second 1: 307 arrived, 100 let throughsecond 2: 306 arrived, 100 let throughsecond 3: 302 arrived, 100 let throughsecond 4: 304 arrived, 100 let throughsecond 5: 346 arrived, 100 let throughsecond 6: 313 arrived, 100 let throughsecond 7: 322 arrived, 100 let throughsecond 8: 300 arrived, 100 let throughsecond 9: 330 arrived, 100 let throughsecond 10: 297 arrived, 100 let through1 s5 s10 s
Let through, per second
100
Beyond the limit
0
Rejected while under it
0
Redis calls a second
0
Added per request
nothing
Central counterevery request checks a shared Redis counter
0200400second 1: 307 arrived, 100 let throughsecond 2: 306 arrived, 100 let throughsecond 3: 302 arrived, 100 let throughsecond 4: 304 arrived, 100 let throughsecond 5: 346 arrived, 100 let throughsecond 6: 313 arrived, 100 let throughsecond 7: 322 arrived, 100 let throughsecond 8: 300 arrived, 100 let throughsecond 9: 330 arrived, 100 let throughsecond 10: 297 arrived, 100 let through1 s5 s10 s
Let through, per second
100
Beyond the limit
0
Rejected while under it
0
Redis calls a second
313
Added per request
1 ms
Local + periodic synccount locally, sync with Redis every 100 ms
0200400second 1: 307 arrived, 130 let throughsecond 2: 306 arrived, 120 let throughsecond 3: 302 arrived, 124 let throughsecond 4: 304 arrived, 119 let throughsecond 5: 346 arrived, 128 let throughsecond 6: 313 arrived, 118 let throughsecond 7: 322 arrived, 124 let throughsecond 8: 300 arrived, 122 let throughsecond 9: 330 arrived, 119 let throughsecond 10: 297 arrived, 122 let through1 s5 s10 s
Let through, per second
123
Beyond the limit
226
Rejected while under it
0
Redis calls a second
40
Added per request
nothing
arrivedlet throughlet through beyond the limitrejected though under the limitlimit, 100/s

Three designs, three failure modes

  • Local limits give each of 4 gateways a quarter of the limit. With traffic spread evenly that is exact and free. But load balancers are rarely even, especially with long-lived connections: when 55% of a tenant’s 81 requests a second land on one gateway, it runs out while the others sit idle, and 189 requests are rejected although the tenant never exceeded its limit. Every time the fleet scales, every share changes.
  • A central counter in Redis is exact whatever the spread: every request increments the tenant’s counter for the current second and is allowed if the count is within the limit. It costs a round trip on every request, about a millisecond inside a data center, and a Redis operation for every request, 313 a second here, and Redis becomes a dependency of every call.
  • Local counts with periodic sync cut Redis traffic: each gateway counts on its own and adds its count to Redis every 100 ms, 40 calls a second instead of 313. Between syncs each gateway knows only its own count, so the fleet lets 123 requests a second through a limit of 100, and during a burst 220. Syncing once a second lets nearly 300 through; syncing every 10 ms is almost exact but makes 400 Redis calls a second, more than the central counter.

In practice the central counter is the default: a Redis call per request is cheap next to the request itself, and one Redis shard handles tens of thousands of operations a second. The sync design suits very high volumes where a few percent of overshoot is acceptable. Either way, gateways can remember locally that a tenant is blocked until the current window ends and skip Redis until then.

Details that matter

  • Atomicity. Read-then-write races let two gateways both see 99 and both allow. Use INCR, which returns the new count, or a Lua script for token buckets and sliding windows.
  • When Redis is down. Decide in advance whether to fail open, allowing traffic and protecting availability, or fail closed, protecting the backend. Many fall back to local limits.
  • Scale. Key counters by tenant, so Redis Cluster spreads tenants across shards; one very hot tenant can still need its own handling.
  • Where to enforce. At the edge for abuse, per tenant at the API gateway, and with load shedding inside services for overload that no per-tenant limit predicts.

A fixed-window limiter on Redis

import time
import redis

r = redis.Redis()

def allow(tenant: str, limit: int = 100) -> bool:
    window = int(time.time())                 # one counter per tenant per second
    key = f"rl:{tenant}:{window}"
    pipe = r.pipeline()                       # MULTI/EXEC: both or neither
    pipe.incr(key)
    pipe.expire(key, 2)                       # old windows delete themselves
    count, _ = pipe.execute()
    return count <= limit

In a system design interview

Say where the count lives and why: “Limits are per API key, enforced in the gateway. Each request does an atomic INCR on a Redis key for the key and the current second, about half a millisecond. Redis is sharded by API key and replicated; if it is unreachable, gateways fall back to a local limit of the global limit divided by the number of gateways, so we degrade to approximate rather than unlimited.” Expect the follow-ups: the boundary burst of fixed windows, hot tenants, and limits across regions.

Read the guide to rate limiting algorithms or API gateways, or watch a strong candidate design a rate limiter.

Questions people ask

How do you implement rate limiting across multiple servers?

Keep the count where every server can see it. The common design is a counter per client and time window in Redis, incremented atomically on every request, with the request allowed if the new count is within the limit. Alternatives split the limit across servers or count locally and sync periodically, trading accuracy for fewer Redis calls.

Why not divide the limit by the number of servers?

Because traffic is rarely spread evenly. With long-lived connections or sticky load balancing, one server can see most of a client’s requests and reject them while the others have unused allowance, so clients under their limit get errors. The shares also have to change every time the fleet scales.

Is Redis fast enough for rate limiting?

Usually yes. An INCR is a sub-millisecond round trip inside a data center and one Redis shard serves tens of thousands of operations a second; sharding counters by client key spreads the load across a cluster. For extreme volumes, gateways batch counts locally and sync periodically, accepting some overshoot.

What happens to a distributed rate limiter when Redis is down?

You choose. Failing open allows all traffic, protecting availability but not the backend. Failing closed rejects traffic, protecting the backend at the cost of an outage. A common middle ground falls back to local per-server limits until Redis returns.

Why must the counter update be atomic?

If a server reads the count, checks it and then writes it back, two servers can read the same value and both allow a request, letting traffic exceed the limit. Redis INCR returns the incremented value atomically, and Lua scripts make multi-step algorithms such as token buckets atomic too.

How much does syncing local counters periodically overshoot?

Between syncs each server only knows its own count, so the fleet can admit roughly the limit plus what every other server allows before the next sync. In the simulation, four gateways syncing every 100 ms let about 23% too many requests through under steady overload and more than twice the limit during bursts.

Where should rate limiting be enforced?

As early as possible for abuse, at the CDN or edge; per client or API key at the API gateway, where identity is known; and inside services as load shedding for overload that per-client limits do not predict. Each layer protects against a different problem.