Rate Limiting

Cap how fast each client can call you: fixed and sliding windows, token and leaky buckets, where the limiter lives, and counting across many servers.

16 minLesson 6 of 6
On this page
  1. The idea
  2. Five ways to count
  3. A token bucket, step by step
  4. Where the limiter lives
  5. Counting across many servers
  6. Numbers worth remembering
  7. In the interview
  8. Common mistakes
  9. What to practise next

Your public API is sized for 3,000 requests a second in total. One customer ships a script with a retry bug, and it starts sending 2,500 a second on its own. Latency climbs for everyone and the database starts timing out. Nobody was malicious; that client had no ceiling.

The idea

Pick a key (an API key, a user id, an IP address), count that key's requests over time, and reject the ones over the limit with 429 Too Many Requests and a Retry-After header saying when to come back. Limits protect capacity, keep customers from hurting each other, enforce plans, and slow attacks such as password guessing.

The cost: a check on every request, shared state across servers, and rejected good traffic if the limit or key is wrong.

Five ways to count

Fixed window. One counter per key per minute, reset each minute. Cheap, but the edges leak: with a limit of 100 a minute, a client can send 100 at 0:59 and 100 more at 1:00, so 200 requests land within about a second, double the limit.

Sliding window log. Store every request's timestamp, drop those older than a minute, count the rest.

Sliding window counter. Keep only this minute's and last minute's counts, and weight the old one by how much of it still overlaps the last 60 seconds. With a limit of 100, 80 requests last minute, 30 so far this minute, and 15 seconds into it: 30 + 80 × 45/60 = 30 + 60 = 90, so the request passes. It assumes last minute was evenly spread: an approximation.

Token bucket. Each key has a bucket that holds up to capacity tokens and refills at rate tokens a second. A request takes one token or is rejected.

Leaky bucket. Requests join a queue drained at a fixed rate; when it is full, new ones are rejected.

Rate-limiting algorithms
OptionGood atBad atPick it when
Token bucket Usual defaultAllows bursts up to a set size, holds the average. Two numbers per key.Two settings to tune; a full bucket releases a burst at once.Most API limits. The usual answer.
Sliding window counterTwo counters per key, no edge spike.Approximate: assumes the previous window was evenly spread.You want "N per minute" without a burst allowance.
Sliding window logExact.Memory grows with the limit: one timestamp per request per key.Low limits where precision matters: 5 login attempts per 15 minutes.
Fixed windowOne counter per key, trivial to build.Up to twice the limit across a window edge.Coarse quotas where the edge burst does not matter: 10,000 calls a day.
Leaky bucketPerfectly even output rate.Adds waiting time; bursts are queued rather than served.Feeding a downstream that needs a steady pace, like a third-party API.

A token bucket, step by step

Capacity 5, refill 1 token a second. No timer refills buckets: each request works out how many tokens arrived since the last one.

Token bucket: capacity 5, refill 1 per secondUse the arrows or ← → keys
on request at time now:  tokens = min(capacity, tokens + (now - last) * rate)  last = now  if tokens >= 1:    tokens = tokens - 1    allow  else:    reject with 429
  1. bucket
    0●
    1●
    2●
    3●
    4●
    now
    0
    tokens
    5
    allowed
    0
    rejected
    0

    1A new client starts with a full bucket: 5 tokens.

  2. bucket
    0●
    1●
    2●
    3●
    4
    now
    0
    tokens
    4
    allowed
    1
    rejected
    0

    2A request at t = 0. No time has passed, so nothing is added. Take a token and allow it.

  3. bucket
    0
    1
    2
    3
    4
    now
    0
    tokens
    0
    allowed
    5
    rejected
    0

    3Four more in the same instant take the last four tokens. A burst of 5 passed: that is what the capacity is for.

  4. bucket
    0
    1
    2
    3
    4
    now
    0.2
    tokens
    0.2
    allowed
    5
    rejected
    0

    4A request at t = 0.2. Refill first: 0 + 0.2 × 1 = 0.2 tokens.

  5. bucket
    0
    1
    2
    3
    4
    now
    0.2
    tokens
    0.2
    allowed
    5
    rejected
    1

    50.2 is less than 1, so reject with 429. The next token arrives in 0.8 s, so send Retry-After: 1, rounded up to whole seconds.

  6. bucket
    0●
    1●
    2●
    3
    4
    now
    3
    tokens
    3
    allowed
    5
    rejected
    1

    6A request at t = 3 refills 0.2 + 2.8 × 1 = 3 tokens.

  7. bucket
    0●
    1●
    2
    3
    4
    now
    3
    tokens
    2
    allowed
    6
    rejected
    1

    7Take one token, pass. 2 left.

  8. bucket
    0●
    1●
    2●
    3●
    4●
    now
    10
    tokens
    5
    allowed
    6
    rejected
    1

    8A request at t = 10. The refill would be 2 + 7 × 1 = 9, but the bucket holds only 5. Idle time cannot be saved up beyond one full burst.

  9. bucket
    0●
    1●
    2●
    3●
    4
    now
    10
    tokens
    4
    allowed
    7
    rejected
    1

    9Take one, allow, 4 left. Over 10 seconds: 7 allowed, 1 rejected. No client can ever get more than 5 + 1 × t requests through in t seconds.

1 / 9
Token bucket (time passed in, in seconds)
class TokenBucket {
  constructor(capacity, ratePerSec) {
    this.capacity = capacity;
    this.rate = ratePerSec;
    this.tokens = capacity;   // start full
    this.last = 0;            // time of the last refill
  }
 
  allow(now) {
    const elapsed = now - this.last;
    this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.rate);
    this.last = now;
    if (this.tokens >= 1) {
      this.tokens -= 1;
      return true;
    }
    return false;
  }
}
class TokenBucket:
    def __init__(self, capacity, rate_per_sec):
        self.capacity = capacity
        self.rate = rate_per_sec
        self.tokens = capacity   # start full
        self.last = 0            # time of the last refill
 
    def allow(self, now):
        elapsed = now - self.last
        self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
        self.last = now
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False

Where the limiter lives

The gateway checks a shared counter store before any request reaches a service. The service keeps its own limit on calls to a fragile third party.API clients to API gateway + limiter (request); API gateway + limiter to Counter store (take a token); API gateway + limiter to Order service (if allowed); Order service to Payment provider (≤ 50/s).API clientsAPI gateway +limiterCounter storeOrder servicePayment providerrequesttake a tokenif allowed≤ 50/s
The gateway checks a shared counter store before any request reaches a service. The service keeps its own limit on calls to a fragile third party.
  • At the edge, by IP: cheap protection against floods.
  • At the API gateway, by API key or user: plan quotas and fairness between customers. This is the usual answer.
  • Inside a service: protecting one dependency, like a payment provider that allows 50 calls a second.

Counting across many servers

Ten gateway nodes with their own counters each allow the full limit, so a limit of 100 a minute lets 1,000 through. Three fixes:

  • A central counter store, an in-memory store such as Redis. Exact, but it costs a hop (about 0.5 ms) per request and adds a dependency. The check and the update must be one atomic operation: if two nodes both read 99 and both write 100, the client got 101. Use an atomic increment or a small script that refills and takes a token in one step.
  • Split the limit: each of the 10 nodes allows 10 a minute. No shared state, but only right if each client's traffic is spread evenly.
  • Local counts, synced every second. Fast; overshoots by what slips through between syncs.

Use the central store for customer quotas, local counters for flood protection. Then decide what happens when the store is down. Fail open (allow everything) for a general API, so a limiter outage is not a full outage. Fail closed, or fall back to strict local limits, for login and other endpoints attackers target.

Numbers worth remembering

  • A token bucket is two numbers per key: about 16 bytes plus the key.
  • A sliding log is one timestamp (8 bytes) per request in the window, per key.
Estimate: Memory for a sliding window log

How much memory does a sliding window log need in the worst case?

You know
  • 1 million active users
  • Limit: 100 requests a minute each
  • 8 bytes per stored timestamp; ignore key overhead

Show the working
  1. Worst case, every user has 100 timestamps stored
  2. 106 × 100 × 8 B = 8 × 108 B = 800 MB

Estimate 800 MB (anything from 300 MB to 3 GB is a good answer)

A token bucket for the same users needs 106 × 16 B = 16 MB, 50 times less.

In the interview

Mention a rate limit when an API is public, an endpoint is expensive, or you call a third party with limits. Name the key, algorithm and response: "Per API key, token bucket of 100 with a refill of 10 a second, enforced at the gateway with counters in a shared store, returning 429 with Retry-After."

Limiting a public API
Interviewer

How would you stop one customer from overloading the API?

You

A per-API-key token bucket at the gateway. It lets a customer burst briefly while capping the average, and it's two numbers per key. With several gateway nodes, the counters go in a shared in-memory store and the check-and-take is one atomic operation.

Interviewer

And if that store goes down?

You

For this API I'd fail open, with each node falling back to a local limit of the total divided by the node count. Losing exact limits for a few minutes is better than rejecting everyone.

Follow-ups to expect:

  • "Why not limit by IP?" Offices and mobile networks share IPs, and attackers rotate them. Use IP only before login; use the account or key after.
  • "And across regions?" Give each region its own counter store and a share of the limit. One global store would add a cross-region round trip to every request.
  • "What should a client do with a 429?" Wait for Retry-After, then back off with jitter.

Common mistakes

Quick check

A fixed window allows 100 requests per minute. What is the most a client can get through in any 2-second span?

A token bucket has capacity 10 and refills 2 tokens a second. It is empty at t = 0. A request arrives at t = 3. How many tokens are left after it is allowed?

You call a partner API that rejects anything over 50 calls a second, and your own traffic comes in bursts. Which approach fits your outgoing calls?

What to practise next

The full design, with rules per endpoint, a counter store across regions and its failure modes, is in Design a Rate Limiter. Counters spread over many cache nodes rely on consistent hashing.