Rate Limiting: Token Bucket, Leaky Bucket, Sliding Window
mediumRate limiting protects services from being overwhelmed by enforcing a maximum request rate per identity. The difference between graceful degradation and cascading failure under load is usually whether you had a working limiter.
Key Concepts
429 Too Many Requests with Retry-After. Headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. Apply at multiple layers — edge (cheap bot defense), gateway (per-API quotas), service (logic-aware). Differentiate authenticated vs anonymous; pre-auth limits on login endpoints prevent credential stuffing.Approach
- Define the identity dimensions: user_id, api_key, ip, tenant_id, endpoint. Pick the right combination per limit.
- Pick the algorithm. Sliding window counter is the practical default.
- Pick the storage. Redis cluster for shared state; local in-pod for hot paths with periodic sync.
- Use Lua scripts for atomicity. INCR + EXPIRE in one round-trip avoids the time-of-check-vs-time-of-use race.
- Return
429 Too Many RequestswithRetry-After,X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset. - Apply at multiple layers: edge (cheap bot defense), gateway (per-API quotas), service (logic-aware).
- Tier-aware limits: free tier strict, pro tier generous; config-driven.
- Pre-auth limits on login / signup endpoints to defend against credential stuffing.
- Emergency override: global circuit breaker that drops low-priority traffic when the system is overloaded.
Algorithms in depth
Token bucket: capacity C, refill R/sec. Each request consumes 1 token. Allows bursts up to C. Most flexible.
Leaky bucket: queue with constant outflow R. Surplus dropped or queued. Hard cap on rate; no bursts.
Fixed window: count in [t, t+window]. Easy to implement, O(1). Boundary problem: 2x rate at window edges.
Sliding window log: per-request timestamps in a sorted set. Accurate, O(N) memory per key. Use for low-volume + high-precision needs.
Sliding window counter: weighted blend of current + previous fixed-window counts. O(1), close to true sliding. Standard default.
Components
- Limiter middleware in the API gateway or sidecar.
- Redis cluster (or local cache + periodic global sync).
- Config service for per-route, per-tier quotas (dynamic, hot-reloadable).
- 429 response builder with Retry-After / RateLimit headers.
- Metrics: rejections per route, per tenant; hot-tenant alerts.
- Bypass / allowlist for internal services.
- Audit log for rejected requests (for support triage).
Distributed correctness
Naive: each pod maintains its own counter. Sum of pod counters > global limit ⇒ over-allowance.
Strict global: every request hits Redis. Adds latency and Redis becomes a SPOF.
Pragmatic hybrid: each pod gets a local 'lease' from the global counter (e.g. 1/Npods of the budget every second), enforces local quota, syncs slowly. Slightly imprecise but linearly scalable.
Cell-based: shard tenants across cells; each cell enforces independently. Bounded blast radius.
Trade-offs
Token bucket: best human-feel (bursts feel natural).
Leaky bucket: hard cap, no bursts. Good for downstream-rate-limited APIs.
Sliding log: best accuracy, worst memory.
Sliding counter: best practical default.
Edge limiting: cheap and absorbs garbage early; can't see user identity until later.
Service-level limiting: logic-aware, more expensive.
Real-world references
- Stripe: published rate-limiting design; multi-tier (load shedding > per-route > per-user).
- Cloudflare: edge rate-limiting with bounded-load global state.
- GitHub: documented per-token rate limits, generous burst, helpful 429 responses.
- Twitter: per-app and per-user limits with reset windows.
- AWS API Gateway: usage plans with token bucket per API key.