Chapter 66: Design Rate Limiter Service
Interview Tip: Start by clarifying requirements around throttling levels (user, API, IP), hard vs soft limits, and distributed consistency needs. Rate limiter questions test your understanding of distributed counters and trade-offs between accuracy and performance.
1. Requirements
Functional Requirements
- Limit requests based on configurable rules (per-user, per-API endpoint, per-IP, global)
- Support multiple tenants with isolated rate limits
- Return clear feedback when limits are exceeded (429 status, retry headers)
- Support different time windows (seconds, minutes, hours, days)
- Allow rule updates without service restart
Non-Functional Requirements
- Low latency: Rate limit check must add minimal overhead (< 5ms)
- High availability: Service must not be a single point of failure
- Scalability: Handle millions of requests per second
- Accuracy: Limits should be enforced with reasonable precision (some over-limit allowed in distributed scenarios)
- Multi-tenant isolation: One tenant cannot exhaust resources for others
Interview Callout: Discuss the trade-off between accuracy and performance. Perfect distributed rate limiting requires coordination overhead. Most systems accept 1-5% over-limit during race conditions.
Capacity Estimation & Infrastructure Sizing
Target Scale
- Target: 1M rate-limited requests/sec across all tenants
- Each rate check = 2 Redis ops: GET current count + INCR. Total: 2M Redis ops/sec needed
Infrastructure Requirements
| Resource | Calculation | Result |
|---|---|---|
| Redis nodes | ~100K ops/sec per node → need 20 master nodes | 20 masters + 20 replicas = 40 total |
| Memory | 1M active keys × 100 bytes (key + counter + TTL) | 100MB (trivial) |
| Network | 1M × 200 bytes request + 100 bytes response | 300MB/sec (fits in 10Gbps NIC) |
| Latency budget | Redis P99 = 1ms, Network = 0.5ms | Total = 1.5ms ✓ (under 5ms requirement) |
Scaling Tiers
| RPS Target | Redis Nodes | Additional Components |
|---|---|---|
| 10K RPS | 1 Redis (master + replica) | Single region |
| 100K RPS | 3 Redis (sharded) | Multi-AZ deployment |
| 1M RPS | 20 Redis (cluster) | Multi-region with rule caching |
| 10M RPS | 200 Redis (large cluster) | Local cache tier to reduce Redis load |
Storage Notes
- Ephemeral data: Counters use TTL = window size (60s, 3600s, etc.)
- No persistent storage needed: All data is in-memory with TTL expiration
- Rule configuration: Stored in persistent DB (PostgreSQL/MySQL), cached in service memory
Interview Callout: Rate limiting is a read-heavy workload with ephemeral data — Redis is perfect. The key insight is that you need 2 ops per check (read + increment), so double your QPS for Redis sizing. Also highlight that counters expire automatically via TTL, so no cleanup jobs needed.
2. High-Level Design
graph TB
Client[Client] --> LB[Load Balancer]
LB --> API1[API Server 1]
LB --> API2[API Server 2]
LB --> API3[API Server 3]
API1 --> RL[Rate Limiter Service]
API2 --> RL
API3 --> RL
RL --> Redis[(Redis Cluster)]
RL --> Rules[(Rule Engine
Config DB)] Redis --> Shard1[Shard 1] Redis --> Shard2[Shard 2] Redis --> Shard3[Shard 3] Admin[Admin Panel] --> Rules style RL fill:#e1f5ff style Redis fill:#ffe1e1 style Rules fill:#e1ffe1
Config DB)] Redis --> Shard1[Shard 1] Redis --> Shard2[Shard 2] Redis --> Shard3[Shard 3] Admin[Admin Panel] --> Rules style RL fill:#e1f5ff style Redis fill:#ffe1e1 style Rules fill:#e1ffe1
Component Breakdown
- API Servers: Application servers that need rate limiting protection
- Rate Limiter Service: Middleware that intercepts requests, checks limits, and returns allow/deny decisions
- Redis Cluster: Distributed cache storing request counters with atomic operations
- Rule Engine: Configuration service managing rate limit rules (limits, windows, entity types)
- Admin Panel: Interface for configuring and updating rate limit rules
3. Algorithm: Sliding Window Counter
We use a sliding window counter algorithm that combines the benefits of fixed window (memory efficiency) and sliding window log (accuracy).
How It Works
- Divide time into fixed windows (e.g., 1-minute buckets)
- Track request count in current window and previous window
- Calculate sliding window count using weighted formula:
count = (prev_count * overlap_percentage) + current_count - Compare against limit and allow/deny request
Example Calculation
Limit: 100 requests per minute
Current time: 10:00:45 (45 seconds into current minute)
Previous window (9:59:00-10:00:00): 80 requests
Current window (10:00:00-10:01:00): 30 requests
Overlap: 15 seconds from previous window (25% of 60s)
Sliding count = (80 * 0.25) + 30 = 20 + 30 = 50 requests
50 < 100, so request is ALLOWED
Interview Callout: Compare algorithms:
- Token Bucket: Smooth burst handling, complex state
- Leaky Bucket: Constant output rate, can delay requests
- Fixed Window: Simple but allows 2x burst at window boundaries
- Sliding Window Counter: Good balance of accuracy and efficiency
4. Rule Engine
The rule engine manages multi-dimensional rate limiting with hierarchical rule evaluation.
Rule Types
| Rule Type | Key Pattern | Example |
|---|---|---|
| Per-User | user:{userId} |
1000 req/hour per user |
| Per-API | api:{endpoint} |
10K req/min for /search |
| Per-User-API | user:{userId}:api:{endpoint} |
100 req/min per user for /upload |
| Per-IP | ip:{ipAddress} |
500 req/hour per IP |
| Per-Tenant | tenant:{tenantId} |
1M req/day per tenant |
| Global | global |
10M req/min system-wide |
Rule Configuration Schema
{
"ruleId": "user_api_limit_001",
"entityType": "user",
"resource": "/api/v1/upload",
"limit": 100,
"window": 60,
"windowUnit": "seconds",
"priority": 1,
"enabled": true,
"tenantId": "tenant_123"
}
Rule Evaluation Order
- Most specific rules first (user + API)
- Medium specificity (per-user or per-API)
- Least specific rules last (global, per-tenant)
- Deny on first rule violation
5. Distributed Implementation
Redis Atomic Operations
Use Redis commands to ensure atomicity in distributed environment:
// Simple approach (race condition possible)
INCR rate:user:123:minute:12345
EXPIRE rate:user:123:minute:12345 60
// Better: Lua script for atomicity
EVAL "
local current = redis.call('INCR', KEYS[1])
if current == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return current
" 1 rate:user:123:minute:12345 60
Sliding Window Lua Script
-- keys: [current_key, previous_key]
-- args: [limit, current_window_start, previous_window_start, window_size]
local current = redis.call('GET', KEYS[1]) or 0
local previous = redis.call('GET', KEYS[2]) or 0
local now = tonumber(ARGV[2])
local prev_start = tonumber(ARGV[3])
local window = tonumber(ARGV[4])
local overlap_pct = (window - (now - prev_start)) / window
local sliding_count = math.floor(previous * overlap_pct + current)
if sliding_count < tonumber(ARGV[1]) then
redis.call('INCR', KEYS[1])
redis.call('EXPIRE', KEYS[1], window * 2)
return {1, sliding_count + 1}
else
return {0, sliding_count}
end
Interview Callout: Discuss why Lua scripts are critical. Without atomicity, race conditions in distributed systems can allow significantly more requests than the limit during high traffic.
6. Response Handling
429 Too Many Requests
When rate limit is exceeded, return HTTP 429 with informative headers:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1620000000
Retry-After: 45
Content-Type: application/json
{
"error": "Rate limit exceeded",
"message": "You have exceeded the rate limit of 1000 requests per hour",
"retryAfter": 45,
"limit": 1000,
"window": "1 hour"
}
Success Response Headers
Include rate limit info in all responses for client awareness:
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1620000000
Client Retry Strategy
- Clients should respect
Retry-Afterheader - Implement exponential backoff for persistent failures
- Monitor
X-RateLimit-Remainingto throttle proactively
7. Scalability and Performance
Redis Cluster Sharding
graph LR
Request[Request] --> Hash[Hash Key]
Hash --> Shard1[Shard 1
user:0-333] Hash --> Shard2[Shard 2
user:334-666] Hash --> Shard3[Shard 3
user:667-999] Shard1 --> Master1[(Master)] Shard1 --> Replica1[(Replica)] style Shard1 fill:#e1f5ff style Shard2 fill:#e1f5ff style Shard3 fill:#e1f5ff
user:0-333] Hash --> Shard2[Shard 2
user:334-666] Hash --> Shard3[Shard 3
user:667-999] Shard1 --> Master1[(Master)] Shard1 --> Replica1[(Replica)] style Shard1 fill:#e1f5ff style Shard2 fill:#e1f5ff style Shard3 fill:#e1f5ff
- Shard Redis by key hash (consistent hashing)
- Each shard handles a subset of users/keys
- Use Redis Cluster or client-side sharding
Caching Rate Limit Rules
- Cache rule configurations in rate limiter service memory
- Refresh cache every 30-60 seconds or on rule updates
- Reduces database load for rule lookups
Performance Optimizations
| Technique | Benefit | Trade-off |
|---|---|---|
| Connection pooling | Reduce Redis connection overhead | Memory for pool |
| Pipelining | Batch multiple checks in one round-trip | Slight complexity |
| Local caching | Allow X% of limit locally without Redis check | Reduced accuracy (over-limit possible) |
| Async logging | Don't block on analytics writes | Eventual consistency |
Handling Failures
- Redis unavailable: Fail open (allow requests) or fail closed (deny all) based on security requirements
- Timeout: Set aggressive timeouts (10-50ms) and fail open to avoid cascading failures
- Split-brain: Use Redis Sentinel or Cluster for automatic failover
Interview Callout: Discuss capacity planning:
- 1M RPS = 1M Redis operations/sec
- Redis can handle 100K-500K ops/sec per instance
- Need 2-10 Redis shards for 1M RPS
- Add replicas for read scaling and failover
8. Monitoring and Observability
Key Metrics
- Rate limit hit rate: % of requests rate limited (per rule, per tenant)
- Latency: P50/P95/P99 of rate limiter check latency
- Redis health: Connection pool usage, operation latency, error rate
- Rule effectiveness: Which rules are most frequently triggered
- False positives: Legitimate users hitting limits (tune rules)
Alerting
- Alert on Redis cluster degradation or failover
- Alert on rate limiter service latency > 10ms P95
- Alert on sudden spikes in rate limit hits (possible attack)
Summary
A production-grade rate limiter service requires:
- Sliding window counter algorithm for accuracy and efficiency
- Redis with Lua scripts for atomic distributed counters
- Flexible rule engine supporting multi-dimensional limits
- Clear HTTP responses with 429 status and retry headers
- Horizontal scaling via Redis sharding and stateless rate limiter services
- Graceful degradation when Redis is unavailable
The design balances performance (sub-5ms latency), scalability (millions of RPS), and reasonable accuracy (1-5% over-limit tolerance in race conditions).