System Design Road

Easy Interview walkthrough

Rate limiter: the interview, played out

A rate limiter decides how many requests each user is allowed, and politely turns away the rest. It sounds like a small feature, but it sits in front of every single request. We’ll build it from one server up, explain every major algorithm along the way, and spend the deep dives on what goes wrong in real life.

17 min read7 interactive diagrams12 sections

This gives away every stage. For a clean attempt, practise it first, then come back.

The short version

  • First ask how strict the limit must be. That one answer decides which designs are allowed.
  • Know the five classic algorithms and what each one gets wrong. Interviewers almost always ask.
  • Many servers must share one count, and checking and updating it must happen in one step.
  • A slow Redis is more dangerous than a dead one. Give up quickly and fall back to a rough local limit.
  • One very busy customer can overload one Redis machine. Count locally for a moment, then sync.

The 45 minutes

Before we start

You’ve seen rate limiting even if you’ve never built it. Try logging in with the wrong password too many times and you get “try again in 15 minutes”. Hammer refresh on a ticket booking site and you get “too many requests”. Use any public API and you’ll find a limit like “1,000 requests per hour”.

That’s the job: count what each user does, and say no when they go over. It protects the servers behind it, and keeps one noisy user from slowing things down for everyone else.

Many candidates treat this as an algorithms question, recite “token bucket”, and stop. The interviewer then asks “what happens when the counting server is slow?”, and that’s where the interview really starts. So we’ll cover the algorithms properly, but also the parts that break in production.

Minutes 0–5Requirements

Interviewer

Interviewer: Chirp is opening a public API. Design the rate limiter. Each API key gets a budget, say 1,000 requests a minute. Requests go through about 50 gateway servers behind a load balancer.

You

You: A few questions first. What are we counting by: API key, user, or IP address?

Interviewer

Interviewer: API key. For the few endpoints that don’t need a key, by IP address.

You

You: Do all requests cost the same? A search is probably heavier than reading one post.

Interviewer

Interviewer: Good point. Search should cost more.

You

You: How strict is the limit? If someone gets 1,030 requests instead of 1,000, is that a problem?

Interviewer

Interviewer: A few percent over is fine. Double is not.

You

You: If a client has been quiet for a while, can they send a quick burst?

Interviewer

Interviewer: Yes, short bursts are fine.

You

You: And if the rate limiter itself breaks, should requests go through or be blocked?

Interviewer

Interviewer: You tell me.

You

You: Fair. Let me write the requirements down, and I’ll come back to that one.

The question about strictness is the most important one, and most people skip it. “A few percent over is fine, double is not” will rule out some designs before we draw anything. And “you tell me” is an invitation: the interviewer wants to see that you know it’s a real decision.

Functional requirements

  1. Limit requests per API key (and per IP address when there’s no key), against a budget we configure.
  2. Let some endpoints cost more than others against the same budget. A search might cost 10, reading a post 1.
  3. When a client is over the limit, reject the request with a clear error that says when to try again.
  4. Let us change a customer’s limit without shipping new code.

Not today: charging customers for going over, and banning abusers permanently. Both use the rate limiter’s data, but they’re separate systems.

Non-functional requirements

  1. Accurate enough: within a few percent of the limit, and never close to double.
  2. Fast: the check runs on every request, so it should add well under a millisecond most of the time, and a few milliseconds at worst.
  3. Doesn’t take the API down: if the rate limiter has a problem, the API should keep working. (That’s our answer to “you tell me”. More in the deep dives.)
  4. Big: about 200,000 requests a second at the busiest time, from about a million different API keys.
  5. Short bursts allowed, within a sensible bound.

Minutes 5–10Back-of-the-envelope estimates

You

You: Every request needs a check, and every check reads and updates a count somewhere. So that’s up to 200,000 count updates a second. One Redis server can do something like 50,000 to 100,000 of these small updates a second, so I’d plan for five or six Redis servers, with each API key’s count living on one of them.

You: Memory is tiny: a million keys at about 100 bytes each is 100 MB.

You: For speed: a round trip to Redis in the same data centre takes a fraction of a millisecond. We can afford one per request, but not two.

You: One more thing: a big partner sending 8,000 requests a second sends all 8,000 updates to the one Redis server that holds their key. Let’s remember that.

WhatWorkingResult
Checks200,000 requests a second, one check each200,000 count updates a second
Redis servers200,000 ÷ ~80,000 per server, plus room to spareabout 5
Memory1 million keys × ~100 bytes100 MB, not a concern
Time per checkone round trip to Redis in the same data centre0.3 to 1 ms
Counting on each gateway1,000 a minute ÷ 50 gatewaysonly 20 a minute each

Notice what the numbers tell us: memory doesn’t matter, but the number of updates and round trips does. Saying “memory isn’t the problem” out loud is useful too. It tells the interviewer you won’t waste time there.

Minutes 10–15Core entities and the API

Core entities

  • Rule: who it applies to (a plan, one customer, or an IP address), the limit, the time period, how big a burst is allowed, and what each endpoint costs.
  • Counter state: for each API key, one or two numbers, depending on the algorithm. Small, and safe to lose.
  • Decision: allowed or not, how much budget is left, and when to try again.

The API

The rate limiter is a check the gateway runs on every request, plus some headers the client sees:

FUNCTIONallow(key, cost)

Inside our system: the gateway asks this before forwarding every request.

Query
key = “ak_9f2…” (the API key), cost = 1 (10 for a search)
Returns
{ "allowed": true, "remaining": 412, "reset_in_ms": 31000 }
429Too Many Requests

What a client sees when they’re over the limit.

Headers
  • Retry-After: 31 (seconds to wait)
  • RateLimit-Limit: 1000
  • RateLimit-Remaining: 0
  • RateLimit-Reset: 31

429 is the standard “too many requests” status code. Retry-After tells the client how many seconds to wait, and almost every HTTP library understands it.

Send the “remaining” header on every response, not just rejections. A well-written client can see its budget running low and slow down before it hits the wall. GitHub’s API does exactly this.

Minutes 15–30The design, built up

Start with the simplest version that works, then let growth break it.

Stage 1One server, counts in memory

API clientsAPI servermap: key → countReset every count each minute.Exact, because there is one counter.
115%
You

You: If there’s only one API server, keep a small table in memory: API key → number of requests this minute. Each request adds one. If the number is over the limit, return 429. When the minute ends, reset to zero.

This is correct, and it’s worth saying why. There’s exactly one count per key, so nothing can disagree with it. It’s also very fast, because nothing leaves the server. We’ll pick a better counting method later. For now this simple “count per minute” is fine.

Stage 2Fifty gateways, fifty separate counts

API clientsLoad balancerNEWGateway × 50each counts aloneNEWChirp API servicesOne key's requests land on 50 counters.Each allows 1,000: up to 50,000 get through.
72%
Green: new in this stage
You

You: Now the API sits behind 50 gateway servers. If each one keeps its own count, one customer’s requests get spread across 50 counts. If each gateway allows 1,000, a customer can actually send up to 50,000 a minute. If each allows 1,000 ÷ 50 = 20, a customer whose requests happen to land unevenly gets blocked long before 1,000.

Interviewer

Interviewer: Couldn’t the load balancer send each customer to the same gateway every time?

You

You: It could, but then one huge customer overloads one gateway. And whenever that gateway restarts, their count resets. It moves the problem rather than fixing it.

This is where “double is not okay” pays off. It rules out separate counts on each gateway.

Stage 3Shared state: one atomic call to Redis

API clientsLoad balancerGateway × 50limiter in-processRedis cluster5 shards, by API keyNEWChirp API services1 script callif allowed
72%
Green: new in this stage
You

You: So the count moves to one shared place: Redis, split across a few servers by API key. Every gateway asks Redis on every request.

Interviewer

Interviewer: Two gateways get a request for the same key at the same moment, with one slot left. What happens?

Two gateways, one slot left
t3
Gateway 7sees 999 → allowGateway 31sees 999 → allowRediscount = 1000API1,001 let through

1,001 requests through, and the counter says 1,000. With 50 gateways and a client at its limit, this happens constantly, and the overshoot grows with exactly the traffic you're trying to limit.

Gateway does
You

You: If a gateway first reads the count, then decides, then writes it back, both gateways can read 999, both say yes, and both write 1,000. Two requests got through when only one should have.

You: The fix is to do read, decide and write as one step inside Redis, so nothing can sneak in between. For a simple counter, Redis’s INCR command adds one and returns the new value in a single step, so each gateway sees a different number. For anything more complex, a small Lua script: Redis runs a script from start to finish before doing anything else.

An operation that happens in one indivisible step like this is called atomic. (Lesson 6.3 walks through Redis scripts for rate limiting.)

One practical detail: when Redis is split across several servers, a script can only touch data on one server. Name your keys so everything for one API key lands together. Redis lets you mark part of a key name with curly braces, like rl:{ak_9f2}, and only that part decides which server it goes to.

Stage 4Production: rules, telemetry and a fallback

API clientsLoad balancerGateway × 50local fallback if Redis is slowRedis cluster5 shards, by API keyChirp API servicesRules serviceplans and overridesNEWLimit eventswho got 429sNEWMetrics and alertsNEW1 script callif allowedpush rules429s, sampled
55%
Green: new in this stage
You

You: Three more things for running this for real. A rules service sends every gateway the current limits, so we can change a customer’s limit without deploying, and gateways don’t look rules up on every request. Gateways record a sample of their 429s, so support can answer “why am I being blocked?” and we get alerted when a big customer hits their limit. And each gateway keeps a rough backup limiter in memory, for when Redis is slow.

Checking the design against the requirements

RequirementWhere the design handles it
Limit per API key, or per IP without a keyOne Redis call per request, keyed by API key or IP
Some endpoints cost moreEach check passes a cost, like 10 for search
Reject with a clear “try again later”429 with Retry-After and remaining-budget headers
Change limits without new codeRules service pushes limits to every gateway
Accurate enough, never doubleShared count in Redis, and an algorithm chosen to stay under 2× (below)
FastOne Redis round trip; rules already in memory
Doesn’t take the API downGive up on Redis quickly and use a local backup (deep dive)
200,000 checks a secondRedis split by key; very busy keys counted locally first (deep dive)
Short bursts allowedA burst allowance in the algorithm (below)

Deep diveEvery rate limiting algorithm, explained

This is the part interviewers almost always ask about, so let’s go through every common algorithm properly. To keep things easy to follow, we’ll use a small example throughout: a limit of 5 requests per minute.

For each one, look at three things: what it remembers, what it gets right, and how it can be fooled.

Fixed window

How it works: split time into fixed blocks, like 12:00 to 12:01, 12:01 to 12:02. Keep one counter per block. Each request adds one. If the counter is already at 5, say no. When the next minute starts, the counter starts again at zero.

What it remembers: one number per user.

Good: the simplest possible design, and very cheap. One INCR in Redis does the whole thing.

The catch: look at the edges of the blocks. A user sends 5 requests at 12:00:59, the counter resets at 12:01:00, and they send 5 more. That’s 10 requests in two seconds, double the limit, and all of them were allowed.

Used by: Envoy’s popular rate limit service counts this way. It’s a fair choice when limits are a loose safety net, not a promise.

Sliding log

How it works: remember the exact time of every request. When a new one arrives, throw away timestamps older than one minute and count what’s left. Fewer than 5? Allow it and add its time.

What it remembers: one timestamp per request in the last minute.

Good: perfectly accurate. In any 60-second stretch, the user never gets more than 5.

The catch: memory. A limit of 5 a minute stores at most 5 timestamps, fine. A big customer allowed 500,000 a minute means storing up to 500,000 timestamps for one key.

Sliding window counter

How it works: a clever middle ground. Keep just two counters: this minute, and the previous minute. To estimate the last 60 seconds, take this minute’s count plus the part of the previous minute that still overlaps. If we’re 15 seconds into the current minute, 45 seconds of the previous minute still count, so:

WhatWorkingResult
Previous minute4 requests, and 45 of its 60 seconds still count4 × 45 ÷ 60 = 3
This minuterequests so far2
Estimate for the last 60 seconds3 + 25, at the limit, so the next request is rejected

What it remembers: two numbers per user.

Good: nearly as accurate as the sliding log, nearly as cheap as the fixed window. Cloudflare measured it on 400 million real requests and found only 0.003% of decisions wrong.

The catch: it assumes last minute’s requests were spread out evenly. Someone who sends all of them in the last second of a minute breaks that assumption, and can get close to double the limit if they time it carefully.

Token bucket

How it works: picture a bucket that holds up to 5 tokens. Each request takes one token; no token, no entry. Tokens drip back in at a steady rate, one every 12 seconds (5 per minute). The bucket never holds more than 5.

What it remembers: how many tokens are left, and when we last added some. Two numbers.

Good: bursts are built in. A user who’s been quiet has a full bucket and can make 5 requests at once, then settles into the steady rate. You can choose the bucket size and the refill rate separately.

The catch: that burst adds to the total. A full bucket of 5, plus 5 more that drip in over the minute, means up to about 10 requests in one minute. The bigger the bucket, the bigger the worst case.

Used by: Stripe’s API rate limiter and AWS API Gateway both describe their limits as token buckets.

A token bucket: holds 10, refills 2 a second
burst 10, then 2/s
2.2 tokensallowed13rejected4+2 tokens / s

Bursts up to 10 are fine; after that, 2 a second. Try the buttons.

Leaky bucket

How it works: the mirror image of the token bucket. Requests pour into a bucket (a queue), and leak out the bottom at a fixed rate, say one every 12 seconds, to be processed. If the bucket is full, new requests are rejected.

What it remembers: the queue of waiting requests, or just how full the bucket is.

Good: the output is perfectly smooth. Whatever arrives, the servers behind it see a steady flow. Great for protecting something fragile.

The catch: bursts don’t get through faster, they wait in line. That adds delay, which is fine for background jobs but annoying for a user waiting on a page.

Used by: NGINX’s built-in rate limiting is a leaky bucket. Shopify describes its API limits the same way.

GCRA

GCRA (the generic cell rate algorithm) behaves exactly like a token bucket, but stores just one number: the time when the user’s next request would be “due” if they sent at exactly the allowed rate. Each request pushes that time forward. If it’s too far in the future, the request is rejected, and the gap tells you exactly how long the client should wait. One number per key, and a free Retry-After. Lesson 6.3 has a step-by-step demo:

GCRA: a rate limit with one number per key
one more
Client10 req/s, burst 5Limiter12.60 − 12.00 > 0.5 → rejectAPI

One more at 12.00 would push TAT to 12.60, more than 500 ms ahead: rejected, with “retry after 100 ms.” One timestamp per key, one compare-and-set per request, no counters or windows to expire. It behaves exactly like a token bucket.

All of them, side by side

AlgorithmRemembers per userBursts?Worst case in any minuteBest for
Fixed window1 numberBy accident, at block edgesAbout 2×Simple safety nets
Sliding logEvery request timeNoExactly the limitSmall limits that must be exact
Sliding window counter2 numbersNoClose to the limit, near 2× if gamedMost APIs
Token bucket2 numbersYes, up to the bucket sizeLimit + bucket sizeAPIs where bursts are welcome
Leaky bucketA queueSmoothed outExactly the rateProtecting fragile backends
GCRA1 numberYes, like a token bucketLimit + burst sizeToken bucket behaviour, cheaper

Here’s the comparison most write-ups skip. Below, a client waits for the best moment and then sends as fast as each algorithm allows. Watch how many requests get through in the worst minute:

Limit 1,000/min. A client waits, then sends as fast as allowed
2.00× the limit
05001,0001,5002,0002,50050s60s70s80s90s100s110s120sseconds (window boundary at 60 s)requests let through in the trailing 60 sthe limitadmittedpeak 2,000

The client sends 1,000 at 59 s and another 1,000 at 60 s, when the counter resets. 2,000 requests in about one second, all within the rules. Anyone who knows your windows are aligned to the clock can do this on purpose.

Algorithm

Which one should you pick?

Interviewer

Interviewer: So which would you use here?

You

You: Our rules are: bursts are fine, but never close to double. A token bucket, or GCRA, with a small burst size, say 10% of the limit. Then the worst minute is about 1.1× the limit, clients still get short bursts, and we store one or two numbers per key. For the few customers whose contract says “never more than X”, I’d use a sliding log and accept the extra memory.

The key thing to say out loud: allowing bursts and never going over the limit pull against each other, and the burst size is the dial between them.

Deep diveWhen Redis gets slow

Interviewer

Interviewer: You said you’d decide what happens when the limiter fails. Decide.

You

You: Let requests through, with a backup. But “Redis is down” is the easy case. The dangerous one is Redis being slow, for example while it switches to a backup server. Every request waits on it.

Redis gets slow, not down
+2 s
Clientstimeouts, retriesGateway6,000 waiting > 512Redisanswers in 1.5 sAPIreceives nothing

Clients time out and retry, which adds load. The API behind is perfectly healthy and receives nothing. The rate limiter, a protective component, has taken the whole API down.

Gateway's Redis call

Bad solutionBlock everything, or wait as long as it takes

If a Redis problem means every request gets rejected, our safety feature has taken the whole API down. Waiting is just as bad. A gateway handling 4,000 requests a second, each waiting 1.5 seconds on Redis, ends up with 6,000 requests stuck at once. It only has a few hundred workers, so within a moment they’re all stuck, and the perfectly healthy API behind them gets nothing. (This “requests in flight = rate × waiting time” rule is Little’s law, from lesson 6.1.) Many Redis libraries wait several seconds by default.

Good solutionGive up quickly and let requests through

Wait only a few milliseconds for Redis, then allow the request. The API stays up. But during the problem there’s no limit at all, so the one abusive client you were holding back now gets unlimited access.

Great solutionGive up quickly, use a rough local limit, and stop asking for a while

When Redis doesn’t answer in time, each gateway falls back to a rough limit of its own: the customer’s limit divided by 50 gateways, kept in memory. Not accurate, but it stops the worst abuse. And after a run of failures, the gateway stops asking Redis at all for a few seconds, then tries again carefully. That’s a circuit breaker (lesson 6.1). Accuracy drops during the problem, but the API stays up.

One exception worth mentioning: for a login page’s “too many attempts” limit, blocking is safer than letting an attacker try unlimited passwords. Saying that shows you decide per endpoint, not by rule of thumb.

Deep diveOne partner, 8,000 requests a second

Interviewer

Interviewer: What happens to the Redis server holding that partner’s count?

You

You: All 8,000 updates a second land on one server, because one key lives on one server. Everyone else on that server slows down too.

Bad solutionAdd more Redis servers

More servers spread different keys around. They can’t split one key, so the busy server stays busy.

Good solutionCount locally, sync every 100 ms

Each gateway keeps a local tally for that key and sends it to Redis every 100 ms in one update (Redis’s INCRBY, which adds any number at once), reading back the total. That’s 50 gateways × 10 updates a second = 500 updates instead of 8,000. The price: between syncs, gateways don’t know what the others have allowed, so a little extra can slip through after the limit is reached.

One partner at 8,000 req/s, 50 gateways syncing to Redis
Redis load
1101001K10K02004006008001000sync interval (ms)per second (log scale)one call per requestbatched100 ms → 500 calls/s

Checking Redis on every request costs 8,000 calls a second on one shard, for one customer. If each gateway counts locally and sends one INCRBY every 100 ms, that's 50 × 10 = 500 calls a second, 16× less, whatever the partner's traffic does.

Show

At 100 ms, the worst case is about 800 extra requests against a limit of 480,000 a minute. That’s 0.17%, well inside “a few percent”. Giving that number is what makes the answer convincing.

Great solutionOnly do it for busy keys, and hand out tokens in advance

Most customers send a few requests a minute and can go to Redis every time. Spot the busy ones and only batch those. Even better, let each gateway take a small batch of tokens from Redis in advance, say 200, and spend them locally. Then the most that can slip through is clear (50 gateways × their unused tokens). And remember who’s already over their limit: Envoy’s rate limiter keeps a local list of keys that are over, and rejects them without asking Redis until the minute rolls over. That matters most during abuse, exactly when Redis would otherwise be busiest.

Deep diveTwo data centres

Interviewer

Interviewer: Now there are two data centres, and one customer’s traffic goes to both.

You

You: Keeping one exact count across both would mean a trip between data centres on every request, maybe 70 to 150 ms. Far too slow. So I’d give up a little accuracy: split each customer’s budget between the two, based on where their traffic usually goes, and adjust it every minute.

You: Or let each data centre count separately and share their totals every second. Then the most a customer can go over is about one second of traffic.

This is a classic trade: speed versus perfect accuracy (lesson 5.4). “A few percent over is fine” is what makes it acceptable.

Deep diveWhat clients do with a 429

Interviewer

Interviewer: Some clients retry immediately when they get a 429. What happens?

You

You: Every retry is another request to check, and it also counts against their budget, so they stay blocked. With thousands of clients doing this, it becomes a flood aimed at our gateways.

You: Fixes: send Retry-After with the exact wait time. Make our official libraries wait longer after each failed attempt, with a bit of randomness so clients don’t all retry at the same moment (lesson 6.1 calls this backoff with jitter). Don’t count rejected requests against the budget. And keep a rejection cheap, with no work behind it.

Then one more idea strong candidates bring up on their own. A rate limiter is about fairness: is this one client using more than its share? It doesn’t know whether the servers are healthy. Load shedding is the other half: when servers are overloaded, drop less important requests, whoever sent them. Stripe describes running both. You need both, because sometimes many well-behaved clients add up to too much at once.

The complete architecture

Here’s everything together: the full design from the last stage, with every part we added along the way. If you only remember one picture from this question, make it this one.

API clientsLoad balancerGateway × 50limiter in-processRedis cluster5 shards, by API keyChirp API servicesRules serviceplans and overridesLimit eventswho got 429sMetrics and alerts1 script callif allowedpush rules429s, sampled
56%

How one request travels through it:

  1. A request arrives at any of the 50 gateways. The gateway already has the rules for this key’s plan in memory, pushed from the rules service.
  2. It makes one call to Redis: a Lua script that reads the current and previous window (or the GCRA timestamp), decides, updates and returns allowed, remaining and reset. One round trip, atomic, so 50 gateways can’t race each other.
  3. If allowed, the request goes on to the API services with rate-limit headers added to the response.
  4. If not, the gateway replies 429 with Retry-After and never touches the backend.
  5. If Redis doesn’t answer within a few milliseconds, the gateway falls back to a local limit (the key’s limit divided by the number of gateways) and lets the request through on that basis. A circuit breaker stops it waiting on Redis at all while Redis is unhealthy.
  6. 429 decisions are sampled into a log so support can answer “why am I being limited?” and so you can alert when a big customer suddenly hits their limit.

How this answer gets scored

Interviewers don’t tick boxes, but their notes on this question tend to land on the same few moments. Here’s what each one sounds like from the other side of the table.

MomentWhat earns creditWhat costs you
The first five minutesAsking how strict the limit is, and what happens if the limiter failsGoing straight to “I’ll use a token bucket”
Fifty gatewaysExplaining why separate counts allow up to 50×One counter per server, and forgetting there are fifty
The shared countOne step inside Redis that reads, decides and writesReading the count, deciding, then writing it back
AlgorithmsKnowing what each one gets wrong, including what bursts addListing names without comparing them
When Redis strugglesGiving up quickly, a local backup, a circuit breaker“Fail open” with no backup, or the default timeout
Busy customersCounting locally and saying how much can slip throughAdding more Redis servers to fix one busy key

What the left column has in common: every choice points back to a requirement or a number from earlier. If you can say where each box came from, the interviewer will notice.