Component Architecture · Phase 2: Core Infrastructure
Rate Limiters & Traffic Shaping
Compare rate limiting algorithms (Token Bucket, Leaky Bucket, Fixed Window, Sliding Window Counter), and design a high-throughput distributed limiter using Redis Lua scripts.
Primary specification: Better Rate Limiting with Redis Sorted Sets (Figma Engineering)
Lesson 17 of 33
1d streakThe Architectural Question
Malicious scrapers or misconfigured client retry loops overwhelm backend APIs with millions of requests, causing resource starvation for legitimate users.
Core Engineering Objectives
- Algorithm trade-offs: Token Bucket vs Leaky Bucket vs Fixed Window vs Sliding Window Counter
- Distributed enforcement: atomic Redis Lua scripts vs local in-memory token bucket approximations
- HTTP 429 semantics: `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining` headers
Interactive system trace
Watch the architecture make its key decision
Switch scenarios, predict the next active component, and step through the request or failure path.
Within budget
Rate Limiting
Client yash has 3 tokens and sends one API request.
Admitted
1
One token spent atomically per request
Rejected
0
All requests fit the budget
Retry after
Not needed
At the selected refill rate
The bucket caps burst size at ten tokens; refill restores tokens over time, never above capacity.
Adjust the bucket
The request is grouped by API key, user, or IP.
Why it matters: Limits need a stable identity and policy scope.
1. What is it & Why it Matters
Every public and internal API is vulnerable to resource exhaustion. Whether from malicious DDoS attacks, credential-stuffing scrapers, runaway customer scripts, or internal services caught in retry storms, unconstrained traffic inevitably saturates database connection pools and takes down infrastructure. Distributed rate limiting enforces admission control at the ingress gateway, protecting downstream systems and guaranteeing tenant fairness.
The Distributed Concurrency Challenge
2. Algorithmic Foundations: 4 Core Approaches
Token Bucket
Tokens refill at a constant rate R up to maximum burst capacity B. Each request consumes 1 token. Permits bursts up to capacity B while bounding long-term average throughput to R. O(1) memory and computation.
Leaky Bucket
Requests enter a FIFO queue and leak out at a strictly constant rate R. Smooths bursty traffic into a steady output stream. Rejects requests when the queue overflows.
Sliding Window Log
Maintains exact timestamps for every request in a sorted set over window [now - window, now]. 100% mathematically exact, but carries severe O(N) memory overhead per user.
Sliding Window Counter
Approximates traffic by weighting counts from current and previous fixed windows: count = current_window_count + previous_window_count * ((window_size - offset) / window_size). O(1) memory with < 0.05% error rate.
| Algorithm | Burst Handling | Memory Footprint | Computational Cost | Boundary Edge Case Vulnerability |
|---|---|---|---|---|
| Fixed Window Counter | Disallows smooth bursts | Lowest (1 integer per user) | O(1) atomic increment | Vulnerable to 2x boundary traffic spikes |
| Token Bucket | Permits bounded bursts (up to B) | Very low (timestamp + token float) | O(1) mathematical refill | None (bounds burst to B, average to R) |
| Leaky Bucket | Smooths bursts to constant rate | Low (bounded FIFO queue) | O(1) processing | None (smooth deterministic output) |
| Sliding Window Log | Permits natural bursts | High (O(N) timestamps per user) | O(log N) sorted set ops | Zero boundary vulnerability (exact precision) |
| Sliding Window Counter | Permits smooth bursts | Very low (2 integers per user) | O(1) mathematical interpolation | Minimal (~0.05% estimation variance) |
3. Naive Collapse & Atomic Redis Lua Implementation
A common production vulnerability is implementing rate limiting using separate Redis GET and SET/INCR operations across distributed API gateway nodes.
NAIVE CHECK-THEN-ACT RACE CONDITION (Client limit: 10 req/min, current count: 9):
Gateway 1: GET count (=9) --------------------------> INCR count (=10) -> ALLOWED
Gateway 2: ------> GET count (=9) -> INCR count (=11) -------------------> ALLOWED (BURST VIOLATION!)
ATOMIC REDIS LUA EXECUTION (Single-Threaded Engine):
Gateway 1 & 2 -> Redis EVAL (Lua Script)
|-- Script runs to completion without interruption
|-- Request 1: Tokens decremented from 1 to 0 -> ALLOWED (HTTP 200)
+-- Request 2: Tokens = 0 -> REJECTED (HTTP 429)-- Production-Grade Atomic Token Bucket in Redis Lua
-- KEYS[1]: ratelimit:{user_id}:tokens, KEYS[2]: ratelimit:{user_id}:last_updated
-- ARGV[1]: capacity (B), ARGV[2]: refill_rate_per_sec (R), ARGV[3]: now_seconds, ARGV[4]: requested (1)
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local last_tokens = tonumber(redis.call('get', KEYS[1]))
local last_updated = tonumber(redis.call('get', KEYS[2]))
if last_tokens == nil then
last_tokens = capacity
last_updated = now
else
local elapsed = math.max(0, now - last_updated)
last_tokens = math.min(capacity, last_tokens + elapsed * refill_rate)
last_updated = now
end
if last_tokens >= requested then
last_tokens = last_tokens - requested
redis.call('set', KEYS[1], last_tokens)
redis.call('set', KEYS[2], last_updated)
local ttl = math.ceil((capacity - last_tokens) / refill_rate) + 60
redis.call('expire', KEYS[1], ttl)
redis.call('expire', KEYS[2], ttl)
return {1, math.floor(last_tokens)} -- 1 = Allowed, remaining tokens
else
return {0, math.floor(last_tokens)} -- 0 = Denied (HTTP 429)
endUse it when
Deploy centralized Redis Lua rate limiting at the API Gateway layer to enforce global tenant quotas and prevent abuse across distributed gateway instances.
Reconsider when
Use local in-memory token buckets (per-container) when handling hundreds of thousands of requests per second where Redis network roundtrips (~1ms) exceed latency budgets.
4. Ingress Protocol, Headers & Stripe 4-Tier Defense
- 01Ingress Interception: Client sends
POST /v1/charges. Gateway extracts IP address and authenticated API key. - 02Atomic Lua Evaluation: Gateway executes Redis
EVALwith token bucket parameters. Redis computes remaining capacity in ~0.5ms. - 03Successful Acceptance: If tokens remain, gateway attaches standard IETF headers:
X-RateLimit-Limit: 100,X-RateLimit-Remaining: 42,X-RateLimit-Reset: 1698421000. Request forwards to downstream services. - 04Rejection (HTTP 429): If tokens < 1, gateway rejects request immediately with
HTTP 429 Too Many RequestsandRetry-After: 4. Zero load reaches internal microservices.
Stripe: 4-Tier Rate Limiting Architecture
Cloudflare: Localized Edge Point of Presence (PoP) Limiting
5. Topology Trade-Offs: Centralized vs. Local vs. Hybrid
| Architecture | Accuracy & Precision | Latency Impact | Fault Tolerance / Failure Mode | Infrastructure Overhead |
|---|---|---|---|---|
| Centralized (Redis Cluster) | Strictly Global (100% consistent across all gateways) | Adds ~0.5 – 1.5ms network hop to every API request | If Redis fails: requires fail-open vs. fail-closed strategy | Moderate (HA Redis cluster) |
| Local In-Memory (Per Gateway) | Approximate (fleet limit = per_node_limit * N_nodes) | Sub-microsecond (< 1 µs in-process memory read) | High resilience (isolated per node) | Zero infrastructure overhead |
| Hybrid Batch Allocation | High (~98% precision via batch synchronization) | Ultra-low (< 0.1ms; local checks with async sync) | Graceful degradation if central store unreachable | Moderate (sync background thread) |
Multi-Region Rate Limiting via Batch Allocation
6. Failure Modes & Interview Gauntlet
- 01The Fail-Open vs. Fail-Closed Dilemma: What happens when the centralized Redis rate-limiter cluster crashes or becomes unreachable?
- 02Fail-Open: Allow all requests through without rate limiting. Preserves availability for legitimate paying users, but risks overwhelming downstream databases during attacks.
- 03Fail-Closed: Reject all requests with HTTP 500/429. Protects downstream databases, but converts a cache blip into a total outage for 100% of legitimate users.
- 04Production Best Practice: Fail-Open with Emergency Local Fallback: If Redis times out (> 10ms), fail open into a conservative local in-memory token bucket on each gateway node to cap catastrophic bursts.
Deep-Dive Q1: What is the boundary-condition flaw of Fixed Window Counters, and how does Sliding Window Counter resolve it?
count = current_count + previous_count * ((window_size - offset) / window_size). At 00:01:01, offset = 1s, yielding count = 100 + 100 * (59/60) = 198 reqs, correctly rejecting the traffic.Deep-Dive Q2: Why should high-scale systems prefer Sliding Window Counter over Sliding Window Log?
ZREMRANGEBYSCORE CPU usage. Sliding Window Counter approximates the exact same behavior using only two integer keys (current and previous window counters), consuming less than 64 bytes per user—a 1,000x reduction in memory overhead with an estimation error under 0.05%.Active Recall Checkpoint
Why must Redis rate limiting logic be executed via Lua scripting rather than multiple individual Redis commands?
Hint: What happens when two API gateway instances check the remaining token count simultaneously?
Show answer & explanation
Executing separate Redis commands creates a check-then-act race condition: two API gateway instances can read the same remaining token count at the same millisecond, both conclude tokens remain, and both decrement, allowing traffic to exceed configured limits. Redis executes Lua scripts atomically on its single-threaded event loop, guaranteeing that no other command or script can run concurrently during token evaluation.
Active Recall Checkpoint
What standard HTTP status code and response headers should an API gateway return when a client exceeds its rate limit?
Hint: Refer to RFC 6585 and IETF conventions.
Show answer & explanation
The gateway must return HTTP 429 Too Many Requests. It should include the following headers: Retry-After: <seconds> (how long to wait before retrying), X-RateLimit-Limit: <max> (the quota capacity), X-RateLimit-Remaining: 0 (indicating exhaustion), and X-RateLimit-Reset: <timestamp> (Unix epoch time when the quota window resets).
Topic sizing snapshot
The four numbers that materially affect this architecture. Use them as bounds, not as a generic calculator embedded in every lesson.
Peak QPS
300,000 RPS evaluation
5-Yr Storage
N/A (Transient counters)
Bandwidth
2 Gbps telemetry traffic
Working Set RAM
16 GB Redis cluster for 50M active user counter keys
Production Postmortem & Verification
Production Incident Postmortem
- Trigger / Setup: A distributed rate limiter checks Redis key and increments counter in two separate network commands.
- Production Symptom: Race condition under concurrency: 50 concurrent requests see count < 100 and all pass through, breaking rate limits.
- Root Cause & Fix: Distributed rate limiting checks and increments must be atomic; execute evaluation inside an atomic Redis Lua script.
Acceptance Criteria & Outcomes
- Evaluate memory footprint and accuracy of Token Bucket, Fixed Window, and Sliding Window Counter.
- Design an atomic distributed rate limiting architecture capable of handling 500,000 RPS with sub-millisecond overhead.
- Define graceful degradation fallbacks when the distributed rate limiting cache cluster fails or partitions.
End-of-Chapter Synthesis & Key Takeaway
Rate limiting protects downstreams from resource exhaustion and abuse. Pick Token Bucket for general traffic bursts, Sliding Window Counter for strict SLA enforcement, and use centralized atomic Redis Lua scripts to eliminate race conditions across distributed API gateways.
Architectural & Technical Interview Review
Core FAANG Interview Questions
Q01How do you choose between the Token Bucket and Sliding Window Counter algorithms?Click to reveal model answer →Hide answer ↑Requirements & Rate Rules
Choose Token Bucket when your system can comfortably tolerate brief bursts of traffic (up to the bucket capacity) while maintaining a steady long-term rate, which is ideal for REST APIs. Choose Sliding Window Counter when you need strict mathematical quota compliance without burst leakage across window boundaries (e.g. strictly 100 requests per minute with no boundary spikes).
Q02How do you implement atomic distributed rate limiting across a cluster of 50 API gateway servers?Click to reveal model answer →Hide answer ↑Request Flows & Architecture
Use centralized Redis clusters executing Lua scripts. A single Redis Lua script runs atomically and sequentially on the Redis server, performing get, increment, and TTL updates in 1 RTT without race conditions. Return standard HTTP 429 Too Many Requests with headers: X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After.
Q03What is the fallback strategy when the centralized Redis rate limiter cluster goes down?Click to reveal model answer →Hide answer ↑Failure Review & Trade-offs
Decide between Fail-Open and Fail-Closed based on the business domain. For billing, security, or payment gateways, Fail-Closed (reject or throttle to protect from fraud). For public user content, social feeds, and search, Fail-Open (allow traffic through to avoid a total outage) while logging alerts and falling back to in-memory local token buckets per gateway node.
Production Pitfalls & Anti-Patterns
Critical Failure Anti-Patterns
Read-Modify-Write Concurrency Race Conditions
Fetching a counter from Redis, incrementing in app memory, and saving it back allows concurrent threads to overwrite each other, allowing users to exceed rate limits by 10x.
Production Mitigation: Execute increments via atomic Redis INCR or custom Redis Lua scripts.
Fixed Window Boundary Burst Spikes
Fixed window counters allow double the quota if a user sends their maximum requests at the end of window 1 and beginning of window 2.
Production Mitigation: Use Sliding Window Log or Sliding Window Counter to smooth traffic across moving intervals.
Rate Limiting by IP in NAT Environments
Limiting by client IP will accidentally throttle an entire university, corporate office, or mobile carrier sharing a single outbound gateway.
Production Mitigation: Rate limit by authenticated user ID or API key, falling back to IP + User-Agent fingerprinting only for unauthenticated endpoints.