Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Design a Rate Limiter

Difficulty: ⭐⭐ | Asked at: Google, Amazon, Stripe, Cloudflare | Time: 30-40 minutes

🎯 Problem Statement

Design a rate limiter that:

  • Limits the number of requests a client can make within a time window
  • Works across distributed servers
  • Supports different rate limiting rules
  • Has minimal impact on request latency

Step 1: Requirements

Functional Requirements

  1. Limit requests based on client ID (API key, IP, user)
  2. Support multiple rate limit rules (e.g., 100 req/min, 1000 req/hour)
  3. Return appropriate HTTP 429 responses when limit exceeded
  4. Provide headers indicating remaining quota

Non-Functional Requirements

RequirementTarget
Latency overhead< 1ms per request
Availability99.99% (fail-open if limiter down)
AccuracyΒ±1% of actual count
Throughput1M+ requests/sec

Step 2: High-Level Design

Where to Place the Rate Limiter

Option 1: Client-side (unreliable β€” client can bypass)
Option 2: API Gateway (recommended β€” centralized)
Option 3: Application-level (flexible but per-server)

Recommended: API Gateway level
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Client  │────→│ API Gateway  │────→│  API Servers  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚ + Rate Limit β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                        β”‚
                 β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”
                 β”‚  Rate Limit  β”‚
                 β”‚   Store      β”‚
                 β”‚  (Redis)     β”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Step 3: Deep Dive β€” Rate Limiting Algorithms

1. Token Bucket

Concept:
β”œβ”€β”€ Bucket holds tokens (max = bucket_size)
β”œβ”€β”€ Tokens added at fixed rate (refill_rate)
β”œβ”€β”€ Each request consumes 1 token
└── If no tokens β†’ reject request

Parameters:
β”œβ”€β”€ bucket_size: Maximum burst capacity
└── refill_rate: Tokens added per second

Example: bucket_size=10, refill_rate=2/sec
β”œβ”€β”€ Allows burst of 10 requests
β”œβ”€β”€ Sustained rate: 2 requests/sec
class TokenBucket:
    def __init__(self, capacity, refill_rate):
        self.capacity = capacity
        self.refill_rate = refill_rate
        self.tokens = capacity
        self.last_refill = time.time()

    def allow_request(self):
        self._refill()
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False

    def _refill(self):
        now = time.time()
        tokens_to_add = (now - self.last_refill) * self.refill_rate
        self.tokens = min(self.capacity, self.tokens + tokens_to_add)
        self.last_refill = now

Pros: Simple, allows bursts, memory efficient Cons: Doesn’t enforce strict window limits

2. Sliding Window Log

Concept:
β”œβ”€β”€ Keep a log of all request timestamps
β”œβ”€β”€ For each new request, count requests in past window
β”œβ”€β”€ If count >= limit β†’ reject
└── Remove expired entries

Example: limit=5 requests/minute
Timestamps: [10:00:01, 10:00:15, 10:00:30, 10:00:45, 10:00:50]
New request at 10:01:02 β†’ Remove 10:00:01 β†’ Count=4 β†’ Allow
New request at 10:01:05 β†’ Count=5 β†’ Reject
class SlidingWindowLog:
    def __init__(self, limit, window_seconds):
        self.limit = limit
        self.window = window_seconds
        self.log = []  # sorted list of timestamps

    def allow_request(self):
        now = time.time()
        window_start = now - self.window

        # Remove expired entries
        self.log = [ts for ts in self.log if ts > window_start]

        if len(self.log) < self.limit:
            self.log.append(now)
            return True
        return False

Pros: Very accurate, no boundary issues Cons: High memory usage (stores every timestamp)

Concept:
β”œβ”€β”€ Combine fixed window counter with sliding window
β”œβ”€β”€ Weight previous window's count based on overlap
└── Much more memory efficient than log

Example: limit=10 requests/minute
Previous window (10:00-10:01): 8 requests
Current window (10:01-10:02): 3 requests
Current time: 10:01:20 (20/60 = 33% into current window)

Weighted count = 8 Γ— (1 - 0.33) + 3 = 5.36 + 3 = 8.36
8.36 < 10 β†’ Allow request
class SlidingWindowCounter:
    def __init__(self, limit, window_seconds):
        self.limit = limit
        self.window = window_seconds

    def allow_request(self, key):
        now = time.time()
        current_window = int(now / self.window)
        previous_window = current_window - 1
        position_in_window = (now % self.window) / self.window

        prev_count = redis.get(f"{key}:{previous_window}") or 0
        curr_count = redis.get(f"{key}:{current_window}") or 0

        weighted_count = prev_count * (1 - position_in_window) + curr_count

        if weighted_count < self.limit:
            redis.incr(f"{key}:{current_window}")
            redis.expire(f"{key}:{current_window}", self.window * 2)
            return True
        return False

Pros: Memory efficient, accurate, smooth Cons: Slightly more complex

4. Fixed Window Counter

Concept:
β”œβ”€β”€ Divide time into fixed windows (e.g., per minute)
β”œβ”€β”€ Count requests in current window
β”œβ”€β”€ If count >= limit β†’ reject
└── Reset count at window boundary

Example: limit=5 requests/minute
Window [10:00-10:01]: 1,2,3,4,5 β†’ 6th request REJECTED
Window [10:01-10:02]: Counter resets β†’ requests allowed

Pros: Simple, memory efficient Cons: Burst at window boundary (2x limit possible)

5. Leaky Bucket

Concept:
β”œβ”€β”€ Requests enter a queue (bucket)
β”œβ”€β”€ Processed at fixed rate (leak rate)
β”œβ”€β”€ If queue full β†’ reject
└── Smooths out traffic spikes

Parameters:
β”œβ”€β”€ queue_size: Maximum queue length
└── leak_rate: Requests processed per second

Pros: Smooth output rate, prevents bursts Cons: Doesn’t allow legitimate bursts, queue delays


Algorithm Comparison

AlgorithmAccuracyMemoryBurst HandlingComplexity
Token BucketGoodO(1)Allows burstsLow
Sliding Window LogExcellentO(n)StrictMedium
Sliding Window CounterVery GoodO(1)SmoothMedium
Fixed WindowFairO(1)Boundary issueLow
Leaky BucketGoodO(n)No burstsLow

Recommendation: Sliding Window Counter for most use cases.


Distributed Rate Limiting with Redis

Challenge: Multiple API servers need consistent rate limiting

Solution: Centralized Redis store

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Server 1 β”‚  β”‚ Server 2 β”‚  β”‚ Server 3 β”‚
β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
      β”‚             β”‚             β”‚
      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
              β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”
              β”‚   Redis   β”‚
              β”‚ (Central) β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Redis Implementation (Atomic):
-- Lua script for atomic check-and-increment
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = tonumber(redis.call('GET', key) or '0')

if current >= limit then
    return 0  -- rejected
else
    redis.call('INCR', key)
    redis.call('EXPIRE', key, window)
    return 1  -- allowed
end

Response Headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1704067260
Retry-After: 30  (only when rate limited)

Rate Limiting Rules

# Example rule configuration
rules:
  - name: "api_general"
    key: "client_id"
    limit: 100
    window: 60  # seconds

  - name: "login_attempts"
    key: "ip_address"
    limit: 5
    window: 300  # 5 minutes

  - name: "expensive_operation"
    key: "user_id"
    limit: 10
    window: 3600  # 1 hour

  - name: "global_limit"
    key: "global"
    limit: 1000000
    window: 1  # 1 million req/sec global

Step 4: Trade-offs

Fail-Open vs Fail-Closed

StrategyWhen Limiter DownRisk
Fail-OpenAllow all requestsAbuse possible
Fail-ClosedReject all requestsService outage

Recommendation: Fail-open with monitoring alerts.

Centralized vs Local Rate Limiting

ApproachConsistencyLatencyComplexity
Centralized (Redis)Strong+1ms networkMedium
Local (per-server)Weak~0msLow

Recommendation: Centralized for accuracy, local for very latency-sensitive paths.

πŸ”— Cross-References