Rate Limiter Design
Token bucket, leaky bucket, fixed window, sliding log and sliding counter as code you can trace, plus how to make one limit hold across many servers.
Overview
A rate limiter decides, for each request, whether a client has used up its allowance for a period of time. It protects a service from overload and abuse, and it is how an API sells tiers. HTTP has a status code for the refusal, 429 Too Many Requests (RFC 6585), with an optional Retry-After header.
The interview has two halves. The first is the algorithm: what state you keep per client, how much memory that is, and how the limiter behaves at the edges of a window. The second is the deployment: a limit that must hold across many API servers needs a shared counter, and a shared counter needs an atomic update. This topic builds each algorithm as a few lines of Python with a trace you can check, then moves the state into Redis.
Five algorithms
- Fixed window — One counter and a window number — Twice the limit around a window edge — Rough — Simple in-process limits
- Sliding log — A timestamp per request — None beyond the limit — Exact — Low limits where memory is fine
- Sliding counter — Two counters — Small overshoot near the edge — Close (Cloudflare reports 0.003% of 400 million requests wrongly allowed or blocked) — Cloudflare's rate limiting
- Token bucket — Token count and last refill time — Up to the bucket size, then the refill rate — Exact for its own rule — Stripe (buckets in Redis), Envoy's local rate limit, AWS API Gateway
- Leaky bucket — A queue or a level and a drain time — Absorbed into a queue, drained at a fixed rate — Exact — Nginx limit_req
Token bucket
A bucket holds up to capacity tokens and gains rate tokens per second. A request takes one token or is refused. Tokens are not added by a timer; the count is computed lazily from the time since the last request, which is why the state is only two numbers.
A trace with capacity 3 and 1 token per second. The first four calls come at t=0.
- 0.0 — 3 — 0 — 3 — allow — 2
- 0.0 — 2 — 0 — 2 — allow — 1
- 0.0 — 1 — 0 — 1 — allow — 0
- 0.0 — 0 — 0 — 0 — refuse — 0
- 0.5 — 0 — 0.5 — 0.5 — refuse — 0.5
- 1.0 — 0.5 — 0.5 — 1.0 — allow — 0
- 10.0 — 0 — 9 — 3 (capped) — allow — 2
The time is passed in rather than read from a clock so the trace is exact and the tests are deterministic; production code passes time.monotonic().
Windows: fixed, log, counter
The three window algorithms answer 'how many requests in the last W seconds' with different amounts of state. The trace uses a limit of 3 per 60 seconds and a client that sends three requests at t=59 and more at t=61.
- Fixed window — allow, allow, allow — allow (and two more) — t=61 starts a new window: 6 requests in 2 seconds
- Sliding log — allow, allow, allow — refuse — Three timestamps in the last 60 s
- Sliding counter — allow, allow, allow — allow, then refuse — Estimate 3 × 59/60 = 2.95 is under 3, so one slips through; the next sees 3.95
Leaky bucket
A leaky bucket is the token bucket seen from the other side: requests pour in, and they drain at a fixed rate. Requests that arrive faster than the drain wait in the bucket (a queue) up to its size, and overflow is rejected. Nginx's limit_req works this way: excess requests are delayed so that they are processed at the configured rate, up to burst of them; beyond that they are rejected, and nodelay rejects instead of delaying. Its default rejection status is 503, which you usually change to 429.
- Output shape — Bursts up to the capacity pass at once — Smooth, one per interval
- Waiting — None: allow or refuse now — Requests can queue, adding latency
- State — Two numbers — A queue, or a level plus a time
- Fits — User-facing APIs (bursts are normal) — Protecting a downstream that must see a steady rate
The interface
Whatever the algorithm, the caller sees one method and one decision. Return enough for the client to behave: whether it may proceed, how many requests remain, and when to retry.
- allowed — Status 200 or 429 — 429 Too Many Requests
- retry_after — Retry-After header (RFC 6585 allows it on 429) — Retry-After: 2
- remaining — An informational header of your choosing — X-RateLimit-Remaining: 0
What you will be able to do
- Implement token bucket, fixed window, sliding log and sliding counter, and trace each on a tiny input.
- Say what state each keeps per client and how that scales to millions of clients.
- Explain the window-edge burst and which algorithms fix it.
- Move the state into Redis with an atomic script, and say what to do when Redis is slow or down.
- Return a decision the client can act on: 429 plus Retry-After.
Interview questions
- Compare token bucket, leaky bucket, fixed window and sliding window rate limiting.
- Implement a distributed rate limiter using Redis.
- How do you make an in-process limiter safe for many threads and many clients?
- How do you handle rate limiting across multiple data centers?
- What should the API return when a client is limited?
Sign up free to read the full topic
More Low-Level Design topics
Start free on Cariara · Coding problems · System design studio