Design a rate limiter for a public REST API that enforces different limits based on subscription tier (free: 100 req/min, paid: 2,000 req/min), keyed by API key, while being fair at scale and providing clear feedback to clients.
Core challenge: Multiple tiers mean the limiter must look up tier config per request, apply per-key counters with different thresholds, and handle tier upgrades/downgrades in real-time without restarting services or losing counter state.
100
req/min (free tier)
2,000
req/min (paid tier)
API Key
rate limit key
429
Too Many Requests
Functional Requirements
What the system must do · the core rate limiting behaviours
Must Have (Core)
1. Enforce per-key rate limits based on API key identity 2. Support tiered plans (free: 100 req/min, paid: 2,000 req/min, enterprise: 10K req/min) 3. Return standard rate limit headers (X-RateLimit-Limit, Remaining, Reset) 4. Reject excess requests with 429 Too Many Requests + Retry-After 5.Real-time tier change propagation when billing tier is upgraded/downgraded
Out of Scope (this design)
✗DDoS mitigation (L3/L4 volumetric attack protection) ✗Geographic distribution (multi-region rate limiting coordination) ✗Per-endpoint limits (this design is per-key only, not per-route) ✗Billing / usage analytics (separate metering pipeline) ✗IP-based rate limiting (complementary but separate concern)
Non-Functional Requirements
The quality constraints that shape every architectural decision
Property
Target
Why It Matters / Design Impact
Latency
<2ms overhead per request
Redis RTT in same datacenter. Rate check must be invisible to end-user experience.
Throughput
100K+ rate-check operations/sec
Every inbound API request hits the rate limiter. Must handle full platform traffic.
Availability
99.99% (fail-open if Redis down)
Rate limiter failure must not become a platform outage. Fail-open with degraded local limits.
Accuracy
±1 request at window boundaries
Sliding window counter provides near-exact enforcement without memory cost of full log.
Scalability
Linear with number of unique API keys
Redis sharding by key hash. Each shard handles a subset of keys independently.
Key tension:Availability vs. Accuracy. We choose availability · if Redis is unreachable, allow traffic through (fail-open) rather than blocking legitimate customers. Sliding window counter trades perfect accuracy for memory efficiency.
Scale Estimation
Given numbers from the question → derive infrastructure sizing
Given (from question):100K active API keys · 50% free (100 req/min) · 40% paid (2,000 req/min) · 10% enterprise (10K req/min)
Step
What to Derive
Calculation
Result
Design Decision
1
Peak rate checks/sec
100K keys checking simultaneously (worst case)
~150K peak
Single Redis instance handles 1M ops/sec · comfortable headroom
Trivial memory footprint. Fits in any Redis instance easily.
4
Redis ops/sec
150K INCR/sec at peak
150K ops/sec
Well within single Redis capacity (1M+ ops/sec). No sharding needed yet.
5
Network bandwidth
150K round trips/sec × ~200 bytes each
~30MB/sec
Standard 1Gbps link handles this easily.
Interview tip: Start with the number of unique API keys (defines memory), then derive peak ops/sec (defines Redis throughput needs). The key insight: rate limiting is extremely lightweight per-key — a single Redis instance can handle millions of keys.
APIs
Rate limit middleware intercepts all API requests · transparent to endpoint handlers
ANY/* (all endpoints)
Rate limit middleware intercepts BEFORE handler execution. Extracts API key from Authorization header or X-API-Key header.
// Response headers (always included)
X-RateLimit-Limit: 2000
X-RateLimit-Remaining: 1847
X-RateLimit-Reset: 1705312260
Retry-After: 12 // only on 429
429Too Many Requests
Returned when sliding window count exceeds tier limit. Client should back off and retry after the specified time.
// 429 Response Body
{
"error": "rate_limit_exceeded",
"message": "API rate limit exceeded for your plan",
"limit": 100,
"reset_at": "2025-01-15T10:31:00Z",
"upgrade_url": "https://api.example.com/billing/upgrade"
}
Data Model
Redis key schemas for rate counters + config DB for tier lookup
Sliding window counter: For a 1-minute window — maintain two buckets (current minute, previous minute). Weighted count = current_count + previous_count × (1 − elapsed_seconds/60). If weighted ≥ tier_limit → reject with 429.
Token bucket alternative: If the API needs burst tolerance (e.g., paid users can burst to 200 req in first second), token bucket is better. Capacity = burst size, refill rate = steady limit. But for strict "100 per minute" semantics, sliding window is cleaner.
Anti-patterns:Fixed window · allows 2× limit at boundary (200 free requests in 2 seconds spanning two minutes). In-memory counters without shared store · user can rotate across servers to bypass. Hardcoded limits · can't change tiers without deployment.