Communication & Reliability· 15 min

Rate Limiting

Token bucket, sliding window, distributed limiting

ReliabilityIntermediate15 min read

Cap the number of requests a client can make in a time window to protect services and enforce fair use.

Rate Limiting — diagram
🖱️ Drag nodes · Scroll to zoom · Drag background to pan
requestYes200 OKNo429R/WClientRate LimiterToken Bucket · 10/sToken available?Backend429 ErrorRetry-After: 60Redisatomic counters

High-level design

Client → [Rate Limiter] → Service. The limiter tracks request counts per key (user ID, IP, API key) and rejects or throttles over-limit requests with 429 Too Many Requests.

Low-level design

Algorithms: Fixed window, Sliding window, Token bucket (tokens refill at rate r, burst up to bucket size), Leaky bucket (smooth output rate). Storage: in-memory (single node), Redis (distributed, atomic INCR + TTL).

Trade-offs

  • →Fixed window is cheap but allows 2x burst at boundaries
  • →Token bucket allows bursts but downstream must handle them
  • →Distributed limiting needs Redis — adds latency

Used in production by

Stripe API limits, GitHub API (5000/hr), Twitter API

Questions to ask your interviewer

  • →Per-user or per-IP limits?
  • →What's the burst tolerance?
  • →Should over-limit requests be queued or rejected?

System design deep dive

Rate Limiting: deep dive

Rate limiting caps how fast any one client can call your system, protecting it from abuse, runaway scripts, and noisy neighbours while keeping the service fair for everyone. In interviews, the depth comes from choosing an algorithm and making it correct across a fleet of servers.

How a distributed rate limiter works, step by step

  1. A request arrives at the gateway or service middleware. The limiter derives a key: API key for authenticated traffic, IP address for anonymous traffic, plus the route, for example rl:key_abc:/search.
  2. It looks up the rule for that key, such as 10 requests per second with a burst of 20.
  3. It performs an atomic check-and-update in Redis using a Lua script, so two gateway instances cannot both spend the last token.
  4. If allowed, the request proceeds and the response includes headers like X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset.
  5. If denied, the limiter returns 429 Too Many Requests with a Retry-After header, and logs the event for abuse monitoring.
  6. If Redis itself is unreachable, the limiter follows a configured policy: usually fail open (allow traffic) for public APIs, or fall back to a coarse local limit.

Algorithms compared

AlgorithmState per keyBurst behaviourNotes
Fixed window counter1 counterUp to 2x limit across a window boundaryCheapest; INCR plus EXPIRE
Sliding window logTimestamp of every requestExactMemory grows with the limit; costly at high rates
Sliding window counter2 countersClose to exactWeights previous window by overlap; good default
Token bucketTokens plus last refill timeAllows bursts up to bucket sizeUsed by AWS and Stripe style APIs
Leaky bucketQueue or level plus timeSmooths output to a constant rateGood for protecting a fragile downstream
Token bucket (single node)
class TokenBucket {
    private final double capacity;      // max burst, e.g. 20
    private final double refillPerSec;  // steady rate, e.g. 10
    private double tokens;
    private long lastRefillNanos;

    TokenBucket(double capacity, double refillPerSec) {
        this.capacity = capacity;
        this.refillPerSec = refillPerSec;
        this.tokens = capacity;
        this.lastRefillNanos = System.nanoTime();
    }

    synchronized boolean tryAcquire() {
        long now = System.nanoTime();
        double elapsedSec = (now - lastRefillNanos) / 1e9;
        tokens = Math.min(capacity, tokens + elapsedSec * refillPerSec);
        lastRefillNanos = now;
        if (tokens >= 1) {
            tokens -= 1;
            return true;
        }
        return false;
    }
}

The distributed version stores tokens and lastRefill in a Redis hash and runs the same arithmetic inside a Lua script, which Redis executes atomically. Note that the bucket is refilled lazily on each request rather than by a background timer, so idle keys cost nothing but memory, and a TTL cleans them up.

When to rate limit (and when something else fits better)

Rate limit public APIs, login and OTP endpoints (to slow credential stuffing), expensive operations such as search or report generation, and outbound calls to third parties that enforce their own quotas. Rate limiting is not a substitute for capacity planning or autoscaling, and it is a blunt tool against distributed attacks from millions of IPs, where edge DDoS protection does better. For internal overload protection, load shedding based on server health (reject when queue depth or CPU is too high) often works better than fixed per-client numbers.

Common mistakes in interviews

  • Keeping counters in each server's memory behind a load balancer, which multiplies the effective limit by the number of servers.
  • Using a non-atomic read-then-write on Redis, creating a race that lets bursts through.
  • Not deciding what happens when the limiter's store is down.
  • Limiting only by IP, which punishes users behind a shared NAT or corporate proxy and is trivially bypassed by attackers with many IPs.
  • Forgetting to tell clients when to retry, which causes aggressive retry loops that make overload worse.

Interview questions

›Why does the fixed window algorithm allow double the limit?

With a limit of 100 per minute, a client can send 100 requests at 12:00:59 and another 100 at 12:01:00, since each falls in a different window. That is 200 requests in two seconds. Sliding windows or token buckets avoid this edge effect.

›Where should the rate limiter live?

Usually at the API gateway or edge so rejected traffic never reaches services, using a shared Redis cluster for counters. Services can add finer limits of their own for expensive operations. Client SDKs can also self-throttle, but they cannot be trusted for enforcement.

›How do you rate limit across multiple data centers?

A single global counter adds cross-region latency to every request. Common approaches are to split the quota per region, or to count locally and asynchronously sync totals between regions, accepting a small overshoot in exchange for low latency.

›Token bucket or leaky bucket?

Token bucket lets clients burst up to the bucket size and then sustain the refill rate, which matches how real users behave. Leaky bucket enforces a smooth constant outflow, which is better when the downstream, such as a legacy system or SMS provider, cannot handle bursts at all.

›Should over-limit requests be dropped or queued?

For interactive APIs, reject quickly with 429 so clients can back off; queuing just turns a fast error into a slow timeout. For background work like webhooks or bulk imports, queuing the excess and processing at the allowed rate is friendlier and loses nothing.

More System Design Concepts

Compare this lesson with the other 13 concepts in the course. Expand any card for its design, trade-offs and diagram.

Category:
Difficulty:
Showing 13 of 13