design·lab

Distributed systems

Rate Limiter

Cap how many requests a client can make per second, so bursts get smoothed and shared resources stay protected — without hand-written per-endpoint throttling.

✗ Problem

One client can take down the API for everyone

No cap on requests → a buggy retry loop, a scraper, or an abusive client floods an endpoint, exhausts CPU / DB connections, and every other user pays the price.

// naive endpoint — no limit at all
app.get('/api/search', (req, res) => {
  runExpensiveQuery(req.query.q)  // ← 10,000 req/s from one client
    .then(r => res.json(r));
});
Shared resources (DB pool, CPU, downstream APIs) get exhausted — legitimate users start seeing timeouts and 500s.
✓ How it works

Token Bucket: a budget that refills over time

A bucket holds up to N tokens and refills R tokens/sec. Each request consumes one token; an empty bucket rejects the request (HTTP 429).

class TokenBucket {
  tokens = N; cap = N; rate = R;

  allow() {
    if (this.tokens > 0) {
      this.tokens--;
      return true;   // 200 OK
    }
    return false;    // 429
  }

  refill() {             // every 1/R sec
    this.tokens = Math.min(this.cap, this.tokens + 1);
  }
}
Refill
R tokens/sec
Bucket
max N tokens
Request
take 1 or reject
Alternatives: fixed window (simple, but bursts at window edges), sliding window (smoother, more bookkeeping), leaky bucket (queues requests out at a fixed rate instead of just counting them).
✓ See it live

Drain the bucket, then refill it

Bucket cap is 5. Click Request to spend a token — empty bucket means rejected (429). Click Tick to simulate one refill interval (+1 token, capped at 5).

Tokens: 5 / 5
0 allowed · 0 rejected
✓ Takeaway

Cheap insurance against runaway clients

  • Protects resources + fairness: one noisy client can't starve everyone else.
  • Smooths bursts: short spikes are absorbed by banked tokens instead of hammering the backend.
  • Key by client: per-user or per-IP buckets, not one global counter.
  • Distributed limiters share bucket state in Redis (or similar) so every API instance agrees on the count.
  • Respond correctly: return 429 Too Many Requests with a Retry-After header.
🎯 Combines / relates: usually a middleware step (Chain of Responsibility) and teams with the circuit breaker for resilience.