This article explains the critical need for API rate limiting to prevent system overload and ensure fair resource allocation. It details the shortcomings of naive fixed-window counters and introduces the Token Bucket algorithm as a superior alternative for handling bursty traffic while maintaining a steady long-term request rate. Practical Python code examples demonstrate both approaches.
Read original on Dev.to #systemdesignRate limiting is a fundamental component in robust distributed systems, serving to protect APIs from abusive behavior, prevent system overload, and ensure service availability. Without effective rate limiting, a sudden surge in traffic—whether malicious or accidental—can lead to resource exhaustion, increased latency, and outright service failures, impacting all users. It's about maintaining stability and fairness in resource consumption.
A common but flawed approach to rate limiting is the fixed-window counter. This method tracks requests within a predefined time window (e.g., 60 seconds) and resets the count at the window's end. While simple, it suffers from a significant drawback known as the "burst problem" or "edge case anomaly."
The Burst Problem
If a user makes `N` requests just before a window boundary and another `N` requests just after the boundary, they can effectively make `2N` requests within a very short period (e.g., 20 requests in ~1 second for a 10 req/min limit). This bypasses the intended limit and can still overwhelm the system. Additionally, fixed-window counters are prone to race conditions in concurrent environments without proper synchronization.
The Token Bucket algorithm addresses the shortcomings of fixed-window counters by modeling rate limits as a continuously refilling resource pool. Instead of strict time windows, it uses a "bucket" that holds a maximum number of "tokens." Each request consumes one token. If no tokens are available, the request is either rejected or delayed.
import time
import threading
class TokenBucket:
def __init__(self, rate, capacity):
"""
rate – tokens added per second (float)
capacity – max tokens in the bucket (int)
"""
self.rate = rate
self.capacity = float(capacity)
self.tokens = float(capacity) # start full
self.timestamp = time.monotonic()
self.lock = threading.Lock()
def _refill(self):
now = time.monotonic()
elapsed = now - self.timestamp
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
self.timestamp = now
def allow(self, cost=1):
with self.lock:
self._refill()
if self.tokens >= cost:
self.tokens -= cost
return True
return False