Phase 2 · Core concepts: scale, reliability and speed · Topic 2.7

Rate limiting and backpressure

Must know

Rate limiting caps how many requests a client can make in a time window. Backpressure is a system telling the senders upstream to slow down when it's overloaded.

Not started

Step 1 Understand it

The problem it solves. Without limits, one buggy script, scraper or attacker can send so many that everyone else gets errors. And a system that accepts more work than it can do eventually collapses for everyone.

New words in this topic (tap one to see what it means)

Watch it happen

AnimationA token bucket: bursts allowed, abuse refused
Read the animation as text
  1. Token bucket: each user has a bucket holding up to 10 tokens, refilled at 1 token a second.
  2. Second 0: a burst of 6 requests. Each takes a token; all 6 go through. Short bursts are fine. 4 tokens left.
  3. Second 1: the bucket has refilled to 5. 6 more requests: 5 get tokens; then the bucket is empty, so the rest (1) get 429 Too Many Requests, with "Retry-After: 1".
  4. Second 4: after a quiet spell the bucket has refilled to 3, so the user's next 3 requests go through. Sustained abuse is held to 1 a second; normal use never notices.
  5. With many gateway servers, each bucket lives in Redis and is updated atomically, so every server shares one count. Inside the system, backpressure does the same job: a full queue stops accepting new work instead of growing forever.

Picture it

flowchart LR
  R["Request from key K"] --> G["Gateway"]
  G --> D{"Token left in K's bucket?<br/>(kept in Redis)"}
  D -->|"yes: take 1"| S["Service handles it"]
  D -->|"no"| X["429 Too Many Requests"]
  T["Refill: +10 tokens a second,<br/>max 20"] -.-> D
Each has a of . A request spends one token; an empty bucket means the request is rejected until the bucket refills.

The idea

protect services from abuse, bugs and traffic spikes, and enforce fair use, like 100 requests per minute per user. Requests over the limit get . Common algorithms: , leaky bucket, fixed window and sliding window. goes further: have size limits, and when they fill, senders are told to wait or requests are dropped on purpose, instead of the whole system falling over.

How it works, step by step

  1. Choose the rule: for example, 10 per key, with bursts of up to 20.
  2. Token bucket: each key has a bucket that holds up to 20 tokens and gains 10 tokens every second.
  3. Each request takes one token. If a token is there, the request goes through.
  4. If the bucket is empty, reply 429 Too Many Requests, with '-After: 1'.
  5. With many , keep each bucket in and update it , so every server shares the same count.
  6. Inside the system, backpressure: when a work queue reaches its limit, stop accepting new work (or drop the least important) instead of letting the queue grow forever.

Analogy

A nightclub bouncer lets in a few people per minute (rate limiting). When the club is full, the queue outside stops moving (backpressure).

Where the analogy breaks

A bouncer is one person at one door. A real service runs on many servers, so the limit must be shared between them, usually with counters in Redis, and that shared store must be fast and always available.

Example

Token bucket: each user's bucket holds up to 10 tokens and refills at 1 token per second. Each request takes a token; an empty bucket means 429 Too Many Requests. Short bursts are allowed; sustained abuse isn't.

Must-know points

  • Token bucket: allows bursts up to the bucket size, with a steady refill rate. The most common.
  • Leaky bucket: processes requests at a fixed rate; smooths out bursts.
  • Fixed window: a simple counter per minute, but allows double bursts at the window edges.
  • Sliding window ( or counter): smoother and fairer, slightly more work.
  • Limit by user, API key or ; return 429 with a Retry-After header.
  • Backpressure: bounded queues, , and signalling clients to slow down.

Common mistakes

  • Limiting per server instead of across all servers.
  • One global limit, so a single heavy user blocks everyone else.
  • Unbounded queues that hide overload until memory runs out.
python
import time

class TokenBucket:
    def __init__(self, capacity, refill_per_sec):
        self.capacity, self.rate = capacity, refill_per_sec
        self.tokens, self.last = capacity, time.monotonic()

    def allow(self):
        now = time.monotonic()
        self.tokens = min(self.capacity, self.tokens + (now - self.last) * self.rate)
        self.last = now
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False                    # the caller returns HTTP 429

Step 2 Go further (free, optional)

Optional. The notes above are written to be enough on their own. These free resources give a second explanation and extra practice.

Step 3 Practise

Try each exercise yourself first: on paper, out loud or in code. Then open the answer and compare it with yours.

  1. ExplainSay it in plain words in under a minute, with no notes.

    Explain the token bucket with the nightclub picture.

    Show answer

    The nightclub bouncer has a bowl of wristbands, and you need one to get in. The bowl holds at most 20, and a helper drops in 10 new ones every second.

    • A group of 15 arrives at once: there are wristbands in the bowl, so they all go in. Short bursts are fine, up to the size of the bowl.
    • A crowd keeps arriving faster than 10 a second: the bowl empties, and people are turned away until new wristbands drop in (HTTP 429, "come back in a second").
    • Over time, nobody gets in faster than 10 a second on average: the refill rate.

    That is the token bucket: capacity = the most you allow in a burst; refill rate = the steady limit. Each request spends one token.

  2. DrawSketch the diagram (or class diagram) from memory.

    Draw where rate limiting happens in a system with a gateway, several servers and Redis.

    Show answer

    The limit is checked once, at the gateway, before any work is done. The counters live in Redis, not in each gateway's memory, so the limit holds no matter which gateway a request lands on. Inside, a bounded queue in front of each service gives backpressure: when it is full, new work is refused instead of piling up.

    text
    Clients
      │
      ▼
    Load balancer
      ├────────────┬────────────┐
      ▼            ▼            ▼
    Gateway 1   Gateway 2   Gateway 3
      │  1. take a token for this
      │     API key (atomic, shared)
      ├───────────────────────► Redis
      │  ◄── allowed / empty ──┘ (one bucket
      │                           per key)
      ├─ empty   → 429, Retry-After: 1
      └─ allowed → service (via a bounded queue)
  3. ApplyUse it in a real design question and say exactly where it fits.

    Implement a fixed-window limiter and the token bucket below. Send a burst of 50 requests to each and compare which get through.

    Show answer

    Both limiters allow 10 requests a second, and both use a fake clock so the result is the same every run.

    A burst of 50 requests in half a second: the fixed window lets the first 10 through and blocks the rest of that second. The token bucket lets 14 through: its 10 stored tokens at once, then about one more every 100 as tokens refill.

    The real difference is at a window edge. 20 requests between 0.9 s and 1.1 s: the fixed window allows all 20 (10 at the end of the first second, 10 at the start of the next), double the limit in 0.2 seconds. The token bucket allows only 11, because tokens don't reset at a boundary; they only refill at the steady rate.

    python
    class FixedWindow:
        def __init__(self, limit, window, clock):
            self.limit, self.window, self.clock = limit, window, clock
            self.current, self.count = None, 0
    
        def allow(self):
            window = int(self.clock() // self.window)
            if window != self.current:                  # a new window: reset
                self.current, self.count = window, 0
            if self.count < self.limit:
                self.count += 1
                return True
            return False
    
    
    class TokenBucket:
        def __init__(self, capacity, refill_per_sec, clock):
            self.capacity, self.rate, self.clock = capacity, refill_per_sec, clock
            self.tokens, self.last = capacity, clock()
    
        def allow(self):
            now = self.clock()
            self.tokens = min(self.capacity, self.tokens + (now - self.last) * self.rate)
            self.last = now
            if self.tokens >= 1:
                self.tokens -= 1
                return True
            return False
    
    
    def run(times):
        now = [0.0]
        clock = lambda: now[0]
        fixed = FixedWindow(limit=10, window=1.0, clock=clock)
        bucket = TokenBucket(capacity=10, refill_per_sec=10, clock=clock)
        fixed_ok = bucket_ok = 0
        for t in times:
            now[0] = t
            fixed_ok += fixed.allow()
            bucket_ok += bucket.allow()
        return fixed_ok, bucket_ok
    
    
    burst = [i * 0.01 for i in range(50)]              # 50 requests in 0.5 s
    edge = [0.9 + i * 0.01 for i in range(20)]         # 20 requests from 0.9 s to 1.1 s
    print("burst of 50:", run(burst))                  # (10, 14): fixed window, token bucket
    print("20 across the 1 s edge:", run(edge))       # (20, 11)
  4. Trade-offsSay what it costs, when not to use it, and the alternative.

    Fail open or fail closed when the limiter's store is down? Decide for a login and for a public search API.

    Show answer

    Login endpoint: don't fail fully open. The limit there is a security control: it is what slows down password guessing and credential stuffing, and attackers look for exactly this kind of gap. Fall back to a strict limit kept in each gateway's own memory (for example, 5 attempts a minute per IP and per account, per server). That is weaker than the shared limit, but far from no limit. If even that isn't possible, fail closed: a short login outage is visible and recoverable; a successful mass account takeover is not.

    Public search API: fail open, with a generous local limit as a backup. Here the limit protects capacity, not secrets. The worst case of failing open is extra load, which and load shedding can absorb, while failing closed would turn a Redis problem into an outage for every user.

    The principle: decide per endpoint, by asking which is worse, abuse getting through or real users being blocked.

Step 4 Interview-ready, then prove it

Your interview answer

About 20 to 40 seconds, said out loud. Practise it until it sounds like you, not like notes.

I'd rate-limit at the API gateway with a token bucket per user or API key, keeping the counters in Redis so every gateway shares them, and return 429 with Retry-After. Internally I'd use bounded queues and load shedding, so overload slows the system down gracefully instead of crashing it.

Follow-up questions

Why is the fixed window unfair?

A client can send the full limit at 12:00:59 and again at 12:01:00, doubling the real rate across the boundary.

What if Redis is down?

Decide to fail open (allow requests and risk abuse) or fail closed (block and risk an outage), per API. Many systems fail open with a local backup limit.

Tick each check when you can do it without notes

A topic is finished when you pass all four checks, not when you finish a video. Be strict: if you needed your notes, it is not ticked yet.

  • Explain Say it in plain words in under a minute, with no notes.
  • Draw Sketch the diagram (or class diagram) from memory.
  • Apply Use it in a real design question and say exactly where it fits.
  • Trade-offs Say what it costs, when not to use it, and the alternative.

0 of 4 checks ticked.

Saved in this browser. Back it up or move it to another device.