system-design/design-rate-limiter.md

42. Design a Rate Limiter Service

Tests: distributed counter consistency, token bucket math, hot key strategies, latency budget.

~4 min read·updated 5/29/2026

42. Design a Rate Limiter Service

Tests: distributed counter consistency, token bucket math, hot key strategies, latency budget.

42.1 Requirements

Functional

  • Allow N requests per time window per (key, route).
  • Reject (or queue) excess.
  • Multiple algorithms supported (fixed window, sliding, token bucket).
  • Configurable per tenant.

Non-functional

  • Low latency (< 10 ms; rate limit check is on every request).
  • High throughput (matches API gateway's QPS).
  • High availability (rate limiter down ≠ service down).
  • Accurate enough (small over-allow OK; under-allow not OK).

(See chapter 23 for algorithm details.)

42.2 Architecture

[ API gateway / app ]──RateLimit check──→[ Rate limiter svc ]
                                                  │
                                       [ Redis cluster (counters) ]
                                                  │
                                       [ Config store (rules) ]

42.3 Storage choice

Redis

The typical pick.

  • INCR + EXPIRE is atomic.
  • Sub-ms latency.
  • Cluster mode for scale.

Limit: a single Redis key handles ~50-100K ops/sec. Hot keys (one tenant = whole limit) need sharding.

In-memory in app

Local enforcement; fast; not coordinated. Used as L1 in front of Redis.

Specialized: Lyft's Ratelimit

Open-source rate limiter using Redis backend, designed to plug into Envoy.

42.4 Algorithms

Token bucket (recommended default)

State: (tokens, last_refill_ts). Lua script atomically refills + checks.

local now = tonumber(ARGV[1]) local rate = tonumber(ARGV[2]) local capacity = tonumber(ARGV[3]) local data = redis.call("HMGET", KEYS[1], "tokens", "ts") local tokens = tonumber(data[1]) or capacity local ts = tonumber(data[2]) or now local elapsed = now - ts tokens = math.min(capacity, tokens + elapsed * rate) if tokens >= 1 then tokens = tokens - 1 redis.call("HMSET", KEYS[1], "tokens", tokens, "ts", now) redis.call("EXPIRE", KEYS[1], 3600) return 1 else return 0 end

Sliding window counter

Two windows: previous and current. Estimated count = prev_count * (1 - elapsed_in_current_window) + current_count.

Fixed window

Simplest. Boundary issues; double burst.

42.5 Hierarchical limits

A request must pass multiple checks:

  • Per-API-key: 1000/min
  • Per-route: /search 100/min
  • Global: 1M/min for the cluster

Each layer is a separate counter; reject on any failure.

42.6 Hot key mitigation

A single tenant limit = a single Redis key.

  • Local pre-check in app: maintain a local approximation; only call Redis when near limit.
  • Sharded counter: for very high QPS tenants, split into N shards; aggregate periodically.
  • Replicate read: if Redis becomes a bottleneck, use sampled reads + occasional truth.

42.7 Latency

Rate limit check is on the hot path. Must be fast.

  • Co-locate Redis with app servers (same AZ).
  • Use Redis Cluster; route to shard owning the key.
  • Avoid cross-region for the counter.
  • Pipelining if checking multiple keys in one request.

42.8 Failure handling

What if the rate limiter is down?

  • Fail open: allow all requests. Easy to abuse.
  • Fail closed: reject all. Outage during outage.
  • Fail soft: use local approximation (last-known limits + small buffer).

Most production systems fail open with monitoring; depend on the assumption that brief abuse < customer impact of total reject.

42.9 Configuration

Limits per (tenant, route, time window) stored in a config service.

  • Versioned.
  • Pushed to rate limiters via watch / polling.
  • Cached in process for fast lookup.

UI for product/support to adjust without deploys.

42.10 Headers

(See chapter 23.) X-RateLimit-* and Retry-After.

42.11 Quotas (longer windows)

Per-day, per-month limits. Same algorithms, longer keys ({tenant}:day:{date}).

For monthly quotas:

  • Counter in Redis with monthly TTL.
  • Reset job at month start (or rely on TTL expiration + reinit).
  • Persist to DB periodically for billing accuracy.

42.12 Distributed sliding window log

(For when precision matters — e.g., financial APIs.)

Each request appends timestamp to a Redis sorted set. Count = ZCARD - ZREMRANGEBYSCORE of old entries.

Memory: O(N) per key. Expensive for high-volume keys.

42.13 Multi-region

Tenant lives in one region (canonical); limits per region.

For globally distributed tenants:

  • Region-local enforcement (eventual consistency); rare cross-region drift.
  • Or async aggregation to a global counter; fall back to local on aggregator failure.

42.14 Burst handling

Token bucket inherently allows bursts up to bucket size. Tune carefully:

  • Larger bucket = more burst tolerance + worse fairness.
  • Smaller bucket = strict but unfriendly to occasional spikes.

For login endpoints: small bucket (strict). For typical API: larger bucket.

42.15 Integration patterns

Envoy filter

Envoy has a built-in rate-limit filter that calls a service via gRPC. Lyft's RateLimit is the reference implementation.

App middleware

Library called by every endpoint. Cleanest for per-business-rule limits (e.g., "can't post more than 5 comments per minute on a thread").

CDN / WAF

Edge enforcement (Cloudflare WAF rate rules). Stops abuse before hitting your app.

Layer all three: edge for crude DDoS; gateway for per-tenant; app for fine-grained.

42.16 Sketch

[Client]──→[CDN(rate)]──→[Gateway(rate)]──→[App(business rules)]
                              │
                       [Rate limiter svc]
                              │
                       [Redis cluster]──→[Config service]

42.17 What an interviewer wants

  • Token bucket as default.
  • Redis as central state with Lua atomic ops.
  • Hot key + hierarchical rate limit awareness.
  • Standard headers, fail-open vs fail-closed trade-off.
  • Multi-layer enforcement (edge / gateway / app).

Key takeaways

  • Token bucket + Redis + Lua = the production answer.
  • Hot key mitigation: local pre-check, shard the counter.
  • Hierarchical limits combine to express realistic policies.
  • Fail-open with monitoring is the safer default if RL goes down.
  • Multi-layer enforcement provides defense in depth.

// 2 views

main
UTF-8·typescript