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

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

  1. Divide time into fixed windows (e.g., 1-minute buckets)
  2. Track request count in current window and previous window
  3. Calculate sliding window count using weighted formula:
    count = (prev_count * overlap_percentage) + current_count
  4. 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

  1. Most specific rules first (user + API)
  2. Medium specificity (per-user or per-API)
  3. Least specific rules last (global, per-tenant)
  4. 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-After header
  • Implement exponential backoff for persistent failures
  • Monitor X-RateLimit-Remaining to 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
  • 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).