Rate Limiting APIs: Strategies and Trade-offs

Rate limiting protects an API from being overwhelmed — whether by a genuine traffic spike, a buggy client stuck in a retry loop, or deliberate abuse — and the algorithm you choose has real behavioral trade-offs.

# Fixed window: simple, but allows bursting at window boundaries
# (e.g. 100 requests at 11:59:59, then another 100 at 12:00:00 = 200 in 1 second)
import time
requests_this_minute = {}

def fixed_window_allowed(client_id, limit=100):
    window = int(time.time() // 60)
    key = f"{client_id}:{window}"
    requests_this_minute[key] = requests_this_minute.get(key, 0) + 1
    return requests_this_minute[key] <= limit

# Token bucket: smoother, allows controlled bursting without the boundary problem
class TokenBucket:
    def __init__(self, capacity, refill_rate):
        self.capacity = capacity
        self.tokens = capacity
        self.refill_rate = refill_rate  # tokens per second
        self.last_refill = time.time()

    def allow(self):
        now = time.time()
        self.tokens = min(self.capacity, self.tokens + (now - self.last_refill) * self.refill_rate)
        self.last_refill = now
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False

Communicating Limits to Clients

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1694208600
Retry-After: 30

Returning these headers on every response (not just when a limit is hit) lets well-behaved clients self-throttle proactively rather than discovering the limit only after being rejected — a meaningfully better API design than silently returning 429s with no advance warning.