Skip to content

Module 07 // Case studiesLesson 31 / 3422 min read

Design a distributed rate limiter

A distributed rate limiter enforces limits like "100 requests per minute per API key" across many servers, quickly and accurately, without becoming a bottleneck or a single point of failure itself.

Think of it like this

Analogy

A nightclub with several entrances and a strict capacity. If each door keeps its own count, the club overfills. So every door radios a central counter before letting anyone in. The counter must answer instantly, never let two doors take the last spot, and if the radio goes down, the doors need a sensible rule of their own rather than locking everyone out.

The problem it solves

Lesson 16 covered rate-limiting algorithms. In production your API runs on dozens or hundreds of servers in several regions, and limits are per API key, per user, per IP and per endpoint, sometimes all at once. Every request must be checked in about a millisecond, the check itself must be correct under concurrency, and the limiter must not take the API down when it has problems.

A real example

Example

A public weather API

A weather API has 10,000 customers, each limited by plan, and requests arrive at 30 gateway servers.

The counters live in Redis, one bucket per API key. Each check runs as a single Lua script that refills the bucket, takes a token and returns allowed or denied in one atomic step, so two gateways can't both take the last token.

To save a Redis call on every request, each gateway grabs 10 tokens at a time for busy keys and spends them locally. That trades a little accuracy for much less load. If Redis becomes unreachable, the gateways fail open: they let requests through and alert the on-call engineer, because blocking every customer would be worse than a few minutes without limits.

How it works

1. Requirements

Functional: configurable rules (key, limit, window, algorithm) per customer tier and endpoint; return 429 with headers when over the limit; update rules without redeploying.

Non-functional: adds under ~1–2 ms per request; accurate within a small margin; highly available; scales to millions of requests per second.

2. Where it runs

As middleware in the API gateway or a sidecar next to each service, so every request is checked before it reaches business logic. Rules are loaded from a config store and cached locally.

3. Counting in a shared store

The usual design keeps counters in an in-memory store such as Redis, partitioned by key with consistent hashing so load spreads across nodes.

Each check must be atomic. For a token bucket, a small server-side script reads tokens and the last refill time, refills, takes a token if available, writes back and returns the result, all as one operation. A sliding window counter uses two per-window counters with expiry.

-- KEYS[1] = bucket key; ARGV = capacity, refill_per_ms, now_ms
local b = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local cap, rate, now = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
local tokens = tonumber(b[1]) or cap
local ts = tonumber(b[2]) or now
tokens = math.min(cap, tokens + (now - ts) * rate)
local allowed = tokens >= 1
if allowed then tokens = tokens - 1 end
redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('PEXPIRE', KEYS[1], math.ceil(cap / rate))
return allowed and 1 or 0
Architecture
Loading diagram…
Every gateway node checks a shared, partitioned counter store before forwarding a request.
  • Clients to Load balancer
  • Load balancer to Gateway 1
  • Load balancer to Gateway 2
  • Gateway 1 to Counter store: check + count
  • Gateway 2 to Counter store
  • Gateway 1 to Rules config
  • Gateway 1 to Backend services: allowed
  • Gateway 2 to Backend services

4. Making it fast and resilient

  • One round trip: do the check in a single atomic call, and pipeline when several rules apply.
  • Local pre-check: once a key is over its limit, cache “blocked until T” locally so further requests are rejected without a network call.
  • Batching for very hot keys: each gateway takes a small allowance of tokens at a time (say 10) and spends them locally. Slightly less precise, far fewer calls.
  • Failure mode: if the store is slow or down, fail open with a conservative local limit and alert; for security-sensitive limits like sign-in attempts, fail closed.
  • Replication: run the store with replicas and automatic failover; losing a few seconds of counts during failover is acceptable.

5. Multiple regions

Strictly global counts need cross-region coordination, which adds latency. Common options: enforce limits per region (simple; a client could get the limit in each region), split the global budget between regions and rebalance periodically, or sync counts asynchronously and accept a small overshoot.

6. Responding

HTTP/1.1 429 Too Many Requests
Retry-After: 7
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 7

Log decisions as metrics (allowed and limited, per rule and tier) so you can tune limits and spot abuse.

Trade-offs

What you gain

  • Accurate, consistent limits across any number of servers.
  • Central rules make tiers and per-customer overrides easy.
  • Local caching and batching keep overhead to about a millisecond or less.

What it costs you

  • The counter store is critical infrastructure that must scale and stay up.
  • Strict global limits across regions cost latency; looser ones allow overshoot.
  • Every request pays a small extra cost.

When not to use it

  • With one server, an in-process token bucket is enough.
  • If approximate limits are fine, per-server local limits divided by the server count are simpler and need no shared store.

In the cloud

Rate limitingEvery major cloud sells this as a managed service, so you configure it rather than build it.

AWS2

Azure2

Google Cloud2

How you’d set it up

Limits are enforced at the edge before requests reach your servers.

  1. CloudFront
  2. AWS WAF rate-based rule
  3. API Gateway usage plan
  4. Backend

Interview questions

Try answering out loud first, then open the model answer.

Design a rate limiter that allows 1,000 requests per minute per API key across 100 servers.

Model answer

Put limiter middleware in the gateway. Rules (key pattern, limit, window, algorithm) live in a config service and are cached locally. Counters live in a Redis cluster sharded by API key; each request runs an atomic Lua token-bucket or sliding-window-counter script in one round trip, returning allow or deny and remaining quota. Denied requests get 429 with Retry-After. Once a key is denied, the gateway caches the block locally until reset. If Redis is unavailable, fail open with a per-server fallback of 1,000 ÷ 100 per minute and alert. Metrics on decisions feed dashboards.

How do you avoid race conditions when two servers update the same counter?

Model answer

Make check-and-update a single atomic operation in the store: an INCR followed by a comparison, a Lua script that refills and takes a token, or a conditional write. Never read the count, decide in the app, then write it back, because two servers can both read 99 and both allow. Atomic operations serialize updates for that key.

How would you rate-limit across three regions?

Model answer

Decide how strict it must be. Usually I'd split the global limit across regions in proportion to recent traffic and enforce each share locally with low latency, periodically rebalancing shares as traffic shifts. That allows small overshoot during rebalancing but keeps each check local. For strict limits on low-volume, high-value operations, a single global store with a cross-region call is acceptable, since a few extra milliseconds don't matter there.

Quiz

Pick an answer to see whether it’s right and why.

  1. 01Why can't each API server just keep its own counters?
  2. 02Two servers read a counter of 99 at the same moment and both allow a request. What fixes this?
  3. 03The rate-limit store becomes unreachable. What's the usual safe default for a public API?
  4. 04How can you avoid calling the shared store on every single request?
  5. 05Why do rate-limit counters usually expire quickly?
  6. 06Your users are spread across several regions. What's a common approach?

0 of 6 answered

Key terms

Rate limiting
Capping how many requests a client can make in a period of time.
Token bucket
A rate-limiting algorithm where requests spend tokens that refill at a steady rate, allowing short bursts.
Sliding window
A rate-limiting algorithm that counts requests in the last N seconds measured from now, avoiding burst gaps at window edges.
429 Too Many Requests
The HTTP status code a server returns when a client is over its rate limit.
Cache
A small, fast store that keeps copies of data that is expensive to fetch, so repeat requests are quicker.
Consistent hashing
A way to map keys to servers on a ring so that adding or removing a server only moves a small share of keys.

Progress is saved in this browser only.