Lesson 62 — System Design VI: Rate Limiter

~25 minutes · system design · Day 80

Tonight's interview shape is common: protect an API from one client, one IP, or one feature suddenly sending too much traffic. A rate limiter is the bouncer before expensive work. It lets normal traffic through, rejects excess traffic early, and gives the interviewer a clean way to test your trade-off thinking.

The invariant

A rate limiter answers “has this key spent its budget in this window?” — token bucket and sliding window are just different bookkeeping for the same budget.

Keep the word key concrete. It might be a user id, API key, IP address, route, tenant, or a combination like user:123:/payments. The whole design is built around checking and updating that key's budget before the request reaches the service.

The script

Use the same system-design script every time: requirements → estimate scale → API → data model → high-level boxes → deep-dive the bottleneck → failure modes.

  1. Requirements. Limit requests per key, return a clear rejection, allow short bursts, and let operators change limits safely.
  2. Estimate scale. If the API sees 10,000 requests per second, the limiter also sees 10,000 checks per second. It must be cheap and close to the API edge.
  3. API. Internally, the service asks allow(key, cost=1). Externally, rejected HTTP requests usually become 429 Too Many Requests, often with Retry-After.
  4. Data model. Store a policy and state per active key: limit, window, remaining budget, and the timestamp needed by the chosen algorithm.
  5. High-level boxes. Put the limiter before the application service. Keep budget state in a fast shared store so all API servers spend from the same budget.
  6. Deep-dive bottleneck. The interesting part is the atomic budget check: check remaining budget and spend one unit as one operation.
  7. Failure modes. Decide fail-open versus fail-closed, watch hot keys, handle clock skew, expose metrics, and make limits configurable.

The architecture diagram

Rate limiter architecture Requests flow from client to API edge, through a rate limiter, into the service when allowed, or to a 429 response when denied. The limiter atomically checks and spends budget in Redis. Client API key / IP API edge auth + route Rate limiter key + policy check, then spend allowed App service expensive work atomic check + spend Redis / budget store tokens, counters, windows Deny early HTTP 429

The diagram's key move is the fork after the limiter. Allowed requests continue to the service. Denied requests never consume database, queue, or worker capacity.

The bottleneck deep-dive

The bottleneck is not drawing boxes. It is making every API server agree on the same remaining budget for the same key. That is why the check and update live in one shared, atomic operation. Redis increments counters in O(1), and Redis Lua scripts can run the multi-step check-and-spend logic atomically on the server.

In an interview, say the trade-off plainly: fixed window is simple, token bucket is smooth with bursts, sliding window is fairer but costs more bookkeeping.

Numbers that matter

FigureWhy it matters
1 check per requestThe limiter sits on the hot path, so the budget decision must be fast and close to the API edge.
O(1) counter updateSimple Redis counters use INCR, documented as O(1), which is why they are common for fixed-window limits.
O(1) bucket stateToken bucket keeps tokens plus last refill time per active key; that stays small even with many users.
O(r) sliding logAn exact sliding log stores request timestamps in the window, where r is requests by that key during the window.
429 rejectionHTTP 429 means Too Many Requests; the RFC says the response may include Retry-After.

Practice — answer before you scroll past

1. Where should the limiter usually sit?

2. Why must check-and-spend be atomic?

3. Which bookkeeping fits smooth refill plus short bursts?

4. When Redis is unreachable, what is the interview answer?

Your move

Redraw the architecture diagram from memory on paper. Then explain the whole design out loud in 5 minutes as if to an interviewer:

  1. State the invariant: budget, key, window.
  2. Walk the 7-step script in order.
  3. Choose token bucket as your default and explain why.
  4. Name two failure modes: Redis down and hot keys.

For the encode step, put one line in your Notion tracker: what the key is, where the budget lives, and which algorithm you chose.

One primary source

After the redraw attempt, read Stripe — Scaling your API with rate limiters. It is a production write-up, and the useful compare step is to notice how they separate request limiting from broader load shedding.


Stuck or curious? Ask your teacher (the agent) — that's what it's for.