InfoWok

Design a Rate Limiter: Where the Counter Lives Decides Everything (2026)

A rate limiter looks like a counter and is really a distributed-systems problem. The prompt, a token bucket, and the decision that matters: where the counters live, what happens when they are unreachable, and why a limiter outage must never become an API outage.

SK
Sukhveer Kaur
Published October 24, 2026
12 min read
Open in ChatGPT Open in Claude
Rate limiter, system design problem 2: gateways check a shared, sharded counter store before letting a request through, beside the line where the counter lives decides everythingSoftware Architecture
DESIGN A RATE LIMITER
On this page +

The interviewer writes one line on the board: “Design a rate limiter.” It sounds like the easy question. A counter, a limit, a 429. The candidate draws a box, writes “Redis” next to it, and says “token bucket”. Ten minutes later the interviewer asks: “What happens when you have twenty gateways?” and then: “What happens when Redis is down?”

Those two questions are the whole problem. A rate limiter is easy on one machine and a distributed-systems question everywhere else. This lesson walks it in 45 minutes; it is the box the URL shortener put in front of link creation.

Intermediate40 min
Before you start
Remember from the sharding lesson

Why does adding a server with key mod N move most keys? Because changing N changes the answer for most keys. A ring or fixed slots move only a small share. The same idea decides where a limiter’s counters live.

Key takeaways
  • A limiter must be cheap, correct under concurrency, and unable to take the API down with it.
  • A token bucket is the default: two numbers per client, bursts allowed, steady average enforced.
  • Counters must be shared and every check must be atomic, or two gateways can both spend the last token.
  • At scale, shard counters by client key and put a small local bucket in front.
  • Decide the failure mode per route: fail open for most, fail closed for login and creation.
  • Answer an over-limit request with 429 and Retry-After, so clients wait instead of retrying at once.
After this lesson you can

Design a rate limiter end to end in 45 minutes: pick an algorithm and defend it, place the counters and make checks atomic, scale the store, and choose what happens when it fails.

Step 1: Pin Down the Problem#

Ask what is being limited, who it is limited by, and what happens to a request that goes over. Write the answers down.

Functional requirements:

  1. Limit how many requests a client can make in a period, per rule (for example, link creation).
  2. Reject requests over the limit with 429 Too Many Requests and tell the client when to retry.
  3. Let an operator change limits without a deploy.

Out of scope (say these out loud):

  • Billing quotas over months, bot detection, and network-level attack protection, which belong to other layers.

Ask first, because each answer changes the design: who is limited (user, API key, IP)? Is the limit per route, and do rules change at runtime? Must it be exact, or is a little overshoot fine? One region or many?

Non-functional requirements, each with a number. These are assumptions you announce:

  • Fast: assume the check adds about 1 to 2 ms, since it runs on every request.
  • Available: if the limiter breaks, the API keeps serving.
  • Accurate enough: a few percent of error is fine; exactness costs latency.
  • Scale: assume the limiter checks every request, redirects included, so about 12,000 checks a second at peak (the URL shortener’s peak) and 1 million client keys.

Underline the second line: the limiter protects the service, so it must never take it down.

Step 2: Entities and API#

The limiter is internal. The gateway calls it, and clients only ever see the response.

text
Rule    route, key, limit, period
check(key, rule)
  -> allowed, remaining, retry_in
 
Over the limit, the client sees:
  429 Too Many Requests
  Retry-After: <seconds>

One entity and one call. Retry-After matters because a client told nothing retries at once. RFC 6585 (April 2012) defines 429 and says it may carry Retry-After, but not how to identify the user or count requests; that is your design. RFC 9110 makes the seconds form a non-negative whole number, so round up. Envoy’s local filter, with an option that is off by default, sends the seconds until the next token, clamped to at least one.

Many APIs also say how much allowance is left. GitHub’s REST API sends these, per its rate-limit docs:

text
x-ratelimit-limit: 5000
x-ratelimit-remaining: 4999
x-ratelimit-reset: <UTC epoch s>

Standard RateLimit and RateLimit-Policy fields are still an IETF Internet-Draft as of October 2026 (draft 11, 23 May 2026), not an RFC. Name the idea; say it is not final.

The state is a key-value record, read and written once per request, with no range queries, so an in-memory store fits and a SQL database (a disk write per request) does not:

text
key   rl:{client}:route
value tokens, last_refill_ms
TTL   capacity / rate seconds
Why that TTL

After capacity / rate idle seconds the bucket would be full anyway, so letting the TTL delete it loses nothing and idle clients cost no memory.

Step 3: A Design That Works#

First, where does the check run? Each place sees a different slice of traffic:

WhereGood atWeak at
Clientsaving wasted callsanyone can skip it
Each servicefull request contexteach copy counts alone
Gatewayone place, before workless context
Separate serviceone shared answeran extra network hop

Pick the gateway: every route passes through it before any real work. A client-side limiter is a courtesy, never the control. Start with one gateway: it finds the client’s bucket in memory, spends a token, and forwards the request or answers 429.

Correct, fast, simple, and where many candidates stop.

Predict first

You now run twenty gateways behind a load balancer. A client’s requests land on all of them. What limit does the client actually get?

Each gateway counts alone, so a limit of 10 a second becomes up to 200 across twenty. Dividing the limit by twenty does not fix it: a client that hits one gateway is held to a twentieth of its allowance.

Pinning a client to one gateway (sticky sessions) breaks on restarts and rebalancing. The counters must be shared.

Step 4: Estimate What Matters#

text
Assumptions: 12,000 checks/s peak,
1 million client keys, about
100 bytes per client
(key, tokens, last refill)
 
Checks: 12,000 /s peak
Memory: 1M x 100 B ≈ 100 MB
Latency: +1 to 2 ms per check
  (one round trip in-region)
Network: 12,000 x 200 B
  ≈ 2.4 MB/s

Each line decides something:

  • 100 MB of counters fits in one machine’s memory, so size is not the problem.
  • At an assumed 12,000 checks a second, one in-memory store is probably enough (check your own limits), but it pushes you to add a local bucket in front.
  • 2.4 MB/s of check traffic is nothing, so bandwidth is not the limit; round trips are.
  • Assume 1 to 2 ms per check, and treat it as the real cost. Pool connections (no handshake per check) and keep work local.

Cross-check: NGINX’s limit_req stores 128 bytes per key on 64-bit platforms, about 8 thousand per megabyte, so a million keys is 125 MB, the same order as our 100 MB. When its zone is full it drops the least recently used state and that client starts over, so size for the keys you expect.

Step 5: Deep Dives#

Four questions decide this design: the algorithm, where the counters live, what happens when they fail, and who is limited. Senior answers pull ahead on the production questions after them, which the follow-ups lesson collects.

Which Algorithm?#

Concept in use: Rate limiting explained

The earlier lesson compares four algorithms. Here the question is which one survives contact with many servers.

Works · Fixed window countercorrect, simplest

Count requests per client per window and reject past the limit. Redis’s rate limiter pattern is a counter per client per second, incremented with an expiry.

Trade-offA client can send a full window just before a boundary and another just after. For 100 a minute that is 200 requests in about two seconds.
Scales · Token buckethandles the load

Each client has a bucket with a capacity and a refill rate. A request spends a token; an empty bucket means 429. Store two numbers, the tokens left and the last refill time, and compute the refill on arrival.

Trade-offBursts up to the capacity are usually wanted but are real load on the backend. Pick the capacity you can absorb.
Survives · Local bucket first, then a global checkhandles failure and growth

Each gateway spends from a small local token bucket, then asks the shared store only for requests that pass it. Envoy’s documentation describes this two-stage shape: a local token bucket absorbs very large bursts that might otherwise overwhelm a global rate limit service, then a fine-grained global limit finishes the job.

Trade-offTwo limits to explain. The local bucket is per gateway, a coarse guard, not the true limit.

Fixing the Window Boundary#

AlgorithmState per keyBurstsWeak spot
Fixed windowone counterup to 2x at a boundarythe boundary
Sliding loga timestamp per requestnone, exactmemory grows with the limit
Sliding countertwo counterssmoothed estimateassumes even spread
Token buckettokens + timeup to capacitythe burst is real load
Leaky bucketqueue + timenone, output is steadyqueued requests wait

A log of 1,000 requests an hour is up to 1,000 timestamps per client (assumption). A sliding counter estimates previous × overlap + current, and Cloudflare’s 2017 write-up gives 42 × (45/60) + 18 = 49.5 and reports 0.003% of 400 million requests wrongly allowed or limited: a measurement on their 2017 traffic, not a general error bound.

A Token Bucket, Worked#

text
capacity 10, refill 5 per second
idle 2 s: min(10, 0 + 5 x 2)
  = 10 tokens
12 requests at once:
  10 allowed, 2 get 429
next token in 0.2 s, so
  Retry-After rounds up to 1

The formula is tokens = min(capacity, tokens + rate × seconds). Elapsed time is all you need; no timer runs per client.

Where Do the Counters Live?#

Concept in use: Database sharding and replication

The counters are data, and the same questions apply: one copy or many, one server or sharding across several servers, and what happens when one is lost.

Works · In each gateway's memorycorrect, simplest

No network, no shared state, and the check costs almost nothing. Envoy’s local rate limiting applies its limit per Envoy process by default.

Trade-offWith twenty gateways the real limit is up to twenty times the configured one, as step 3 showed. It is fine as a coarse guard and wrong as the only limit.
Scales · A shared store with one atomic checkhandles the load

Every gateway asks one counter store, such as Redis. The check must be a single atomic step, because a read followed by a write has a race:

text
bucket has 1 token
A reads 1     B reads 1
A writes 0    B writes 0
-> both allowed, 1 token spent

Redis guarantees a Lua script’s atomic execution: its effects either have yet to happen or had already happened. Put the whole check (read, refill, spend, save) in one script.

  • Take the time from the store’s own clock, so gateways with different clocks cannot disagree about the refill.
  • Redis documents a leak in the plain INCR pattern: a client that dies before EXPIRE leaves a key that never expires. A script makes both one step.
Trade-offEvery request pays a round trip, and the store is now on the critical path of every call. It is also one machine’s capacity, so it eventually needs splitting.

When One Store Is Not Enough#

Survives · Sharded counters, replicas and a local buckethandles failure and growth

Split the counters across several stores by hashing the client key, so each client always lands on the same shard. A ring or fixed slots (consistent hashing) avoid moving most keys when you add a shard. Redis Cluster, for example, splits the key space into 16,384 hash slots and moves whole slots between servers.

Give each shard a replica for failover, and keep the local bucket from the first deep dive in front so most requests never leave the gateway.

Trade-offReplicas are usually asynchronous (Redis Cluster’s specification says a failover can lose acknowledged writes), so a few recent counts can be lost and a few extra requests let through. Acceptable: the limit is approximate by design. More shards are more to operate.

What Happens When the Limiter Is Down?#

This is the question that separates senior answers. A limiter that fails can either let everything through or stop everything.

Works · Fail opencorrect, simplest

If the check errors or times out, allow the request and alert.

Trade-offThe API stays up, but there is no limit during the outage, so an abuser runs free.
Scales · Fall back to a local limithandles the load

If the shared store is unreachable, use the gateway’s local bucket. It still stops a flood from one client.

Trade-offThe limit is loose, up to the number of gateways times the local limit. Use a short timeout so a slow store does not slow every request.
Survives · Decide per routehandles failure and growth

Fail open or fall back for reads and ordinary calls. Fail closed for sensitive routes such as login and link creation, where unlimited attempts are the very risk the limiter exists to stop.

Trade-offFail-closed routes go down with the limiter, so test the policy. A circuit breaker avoids hammering a store that is already struggling.

Real gateways make this a setting. In Envoy’s global rate limit filter, failure_mode_deny: true returns a 500 when the limit service fails; the API reference shows a plain boolean, so unset means fail open, with a 20 ms default timeout. Stripe’s 2017 write-up also argued for failing open.

Who Is Limited, and What Do They See?#

Works · Limit by IP addresscorrect, simplest

Simple and needs no login.

Trade-offMany users can share one address behind a corporate network or a mobile carrier, so one heavy user can block the rest, and one user can use many addresses.
Scales · Limit by API key or user, falling back to IPhandles the load

Authenticated traffic is limited by key or user ID, anonymous traffic by IP with a tighter limit.

Trade-offYou must authenticate before limiting, or accept that the limiter sees only the IP for unauthenticated calls.
Survives · Composite keys and planshandles failure and growth

Key on user and route together, so heavy use of one endpoint does not exhaust the rest, and give each plan its own limits from a rules store that the gateways reload without a deploy.

Trade-offMore keys, more memory and more rules to explain to customers.

When several rules match (per user, per route, per IP), check them all and let the most restrictive decide. On Redis Cluster one script can touch several keys only if they share a slot; a hash tag such as {user42} in each key name keeps one user’s buckets together.

A hot key is a client that sends far more than the rest:

  • An empty local bucket rejects without asking the store, so the shard is spared.
  • Abusive sources go on a short-lived blocklist at the edge, before any counter is touched.
  • A legitimate heavy user gets a bigger plan, not an exception.

Clients have a part too. GitHub’s guidance: wait for retry-after, else until x-ratelimit-reset, else at least a minute, then back off exponentially. Add jitter so clients do not retry in the same second.

The Final Design in One Picture#

One request: the gateway spends a local token, asks the shard that owns the key to run the atomic check, then forwards the request or answers 429 with Retry-After. If the shard is unreachable it uses the local bucket or rejects, by route.

Gateways load limits from a rules store, by polling (simple, slower) or push (instant, one more system), and keep the last good rules in memory.

What Comes After the Core Design#

The follow-up questions (what breaks, what is abused, what it costs) go deeper than the core answer. They live in Design a rate limiter: failure modes, security and follow-ups.

Follow-upWhere to read it
Each part dies, forged client addresses, DDoSFailure modes and security
Reject, delay or shed, cost-based LLM limits, several regionsPolicy and scale
Shadow-mode rollout, the 45-minute plan, probe questionsOperations and the round

What Each Level Should Show#

LevelExpected in the room
Mid-levelA token bucket, a counter in Redis, 429 on over-limit. The interviewer may raise the multi-server problem.
SeniorRaises the race condition and atomic scripts, shards by client key, places a local bucket in front, and picks a failure mode per route.
Staff+Approximate versus exact trade-offs, replica lag, hot clients, rules rollout and testing, cost-based LLM limits, and what to build first.

Red Flags and Green Flags#

✗ Red flag

“I’ll keep a counter in memory and check it on each request.” Nothing about several gateways, races, or what happens if the store is down.

✓ Green flag

“One gateway is trivial, so the interesting parts are shared counters and atomic checks, then what happens when the store fails. I’ll fail open for reads and closed for login.”

More red flags: limiting by IP only, a fixed window with no word on its boundary, no Retry-After, and failing closed everywhere. More green flags: every number comes with what it drives, and every component gets “what if it dies?”

The most common way to lose this round

Spending fifteen minutes on token bucket versus leaky bucket and never reaching the distributed part. The interviewer is waiting for “what happens with twenty gateways?”

Try It: Limit Login Attempts#

Exercise · Design the limiter for a login endpoint15 min

Change one variable: the route is login, and the rule is 5 attempts per 15 minutes per account. Decide:

  1. What is the key?
  2. Which algorithm and what numbers?
  3. What happens if the limiter is down?
  4. What does a blocked user see?
Hint

Fifteen minutes divided by five attempts gives the refill interval. Then ask what an attacker gains if the limiter fails open.

One reasonable answer
text
Key: account id, plus IP
Bucket: capacity 5
Refill: 1 per 3 min
  (15 min / 5 attempts)
Burst: 5 tries at once
Down: fail closed on login
Reply: 429 + Retry-After

Key on the account, not only the IP, or an attacker rotates addresses. Fail closed: with the limiter down, unlimited password guesses are the risk, so refuse logins briefly. Tell the user how long to wait, and alert on spikes of blocked accounts.

Check Yourself#

Check yourself3 questions · answer, then open
1.Why can a fixed window let through twice the limit?

A client can spend a full allowance at the end of one window and another at the start of the next, so up to twice the limit lands around the boundary.

2.Two gateways each read a bucket with one token left and both allow a request. What went wrong and how do you fix it?

Read and write were separate steps, so both saw the same token. Make the check one atomic step, such as a Lua script in Redis.

3.The counter store is down. What do you do for reads and for login?

Reads: fall back to a local limit or fail open. Login: fail closed, because unlimited attempts are the risk. Alert in both cases.

In One Picture#

text
Requirements
  limit per key, 429 + retry,
  rules without a deploy
Scale
  12,000 checks/s · 100 MB
Algorithm
  token bucket, two numbers
Counters
  shared, atomic, sharded
Local bucket
  absorbs bursts first
Failure
  open or local; closed for
  login and creation
Operations
  shadow new rules first (see
  the follow-ups lesson)

Conclusion#

A rate limiter is a small data structure wrapped in a hard question: where do the counters live, and what happens when they cannot be reached? Share them, check atomically, shard by client, keep a local bucket in front, and choose the failure mode per route. That is the 60-second answer.

Which failure mode would you defend first in the room? Tell us in the comments.

Where to go from here

Frequently asked questions

What is the best algorithm for a rate limiter? +
A token bucket is the usual default. It allows a short burst and a steady average rate, and it needs only two numbers per client: the tokens left and when they were last refilled. A fixed window is simpler but can let through up to twice the limit around a window boundary.
How do you rate limit across many servers? +
Keep the counters in a shared store that every gateway asks, and make each check atomic so two gateways cannot both spend the last token. At larger scale, shard the store by client key, add replicas, and put a small local bucket in front to absorb bursts.
Should a rate limiter fail open or fail closed? +
It depends on the route. For most traffic, fail open or fall back to a local limit so a limiter outage does not take the API down. For sensitive routes such as login or account creation, fail closed, because allowing unlimited attempts is the risk the limiter exists to prevent.
What should a rate-limited response look like? +
HTTP 429 Too Many Requests, with a Retry-After header telling the client how many seconds to wait. Without it, clients retry immediately and make the overload worse.

References

  1. Redis documentation — INCR, Pattern: rate limiter
  2. Redis documentation — Scripting with Lua (atomic execution)
  3. Envoy documentation — Local rate limit filter (token bucket, 429, Retry-After)
  4. Envoy documentation — Global rate limiting
  5. RFC 6585 — Additional HTTP Status Codes (429 Too Many Requests)
  6. RFC 9110 — HTTP Semantics (Retry-After)
  7. NGINX documentation — ngx_http_limit_req_module
  8. Stripe engineering — Scaling your API with rate limiters (2017)
  9. Cloudflare blog — How we built rate limiting capable of scaling to millions of domains (2017)
  10. Envoy documentation — Global rate limit filter (failure_mode_deny)
  11. Envoy API reference — RateLimit filter config (timeout, failure_mode_deny)
  12. Redis documentation — Cluster specification (hash slots, hash tags, asynchronous replication)
  13. GitHub Docs — Rate limits for the REST API
  14. IETF — RateLimit header fields for HTTP (Internet-Draft, draft 11)
Written by
Sukhveer Kaur
Sukhveer KaurSoftware Developer & AI Engineer

Sukhveer is a software developer specialising in AI systems and backend engineering. She has hands-on experience designing agentic AI applications, working with large language model pipelines, autonomous agent frameworks, and cloud-native services in Java and Python. At InfoWok, she bridges the gap between cutting-edge AI research and practical implementation — helping developers understand and apply emerging technologies through clear, experience-backed writing.

New AI engineering guides, the day they ship

Real Python, production depth. No digest spam.

Comments