Designing a Rate Limiter: A Practical System Design Walkthrough

The rate limiter is a system design classic for good reason: it's small enough to design in an hour, yet it forces you through real distributed-systems trade-offs: accuracy vs memory, consistency vs latency, what happens when your dependencies fail. I've both built one for a production API and used it as an interview exercise. Here's the walkthrough I'd give either audience.

Requirements first

Before algorithms, pin down the contract: limit requests per client (API key, user ID, or IP) to N per time window; reject excess with HTTP 429 and a Retry-After header; add minimal latency (single-digit milliseconds); and work correctly when the API runs on many servers. That last requirement is the one that shapes the whole design.

Choosing the algorithm

I default to token bucket: clients get burst tolerance, you get a steady average, and the state per client is just two numbers: token count and last-refill timestamp.

Where the limiter lives

In-process middleware is fine for one server. The moment you run multiple instances behind a load balancer, per-server counters mean a client's effective limit is N × servers. So state moves to a shared store. Redis is the standard answer: fast, supports atomic operations, and TTLs expire idle clients' state automatically. The check must be atomic (read tokens, refill, decrement, write back), otherwise two concurrent requests both pass on the last token. A small Lua script executed in Redis does the whole sequence as one atomic operation.

The distributed-systems questions

The API contract matters too

Return 429 Too Many Requests with Retry-After, and include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset on every response; well-behaved clients will throttle themselves before you have to. That single practice reduced our actual 429 rate more than any tuning of the algorithm did.

The pattern to take away: the algorithm is the easy 20%. The real design work is state placement, atomicity, failure behaviour, and the client contract, and that's true of most system design problems, not just this one.

Frequently asked questions

Which algorithm should I use for a rate limiter?

Token bucket is the best default: a bucket of N tokens refills at a steady rate and each request spends one. It allows short bursts while enforcing the average rate, and per-client state is just two numbers (token count and last-refill timestamp). Sliding window counter is a solid alternative.

How do you rate limit across multiple servers?

Move the counters to a shared store, usually Redis, and make the check-and-decrement atomic with a small Lua script. Per-server in-memory counters silently multiply a client's effective limit by the number of servers.

Should a rate limiter fail open or fail closed when Redis is down?

Decide explicitly. For most public APIs, fail open (allow traffic, alert loudly) because rate limiting is protection rather than a security boundary; fail closed only when the backend must be protected at the cost of rejecting users.

← All posts