This article discusses the critical role of rate limiting in preventing system overload, illustrated by a real-world incident. It advocates for the token bucket algorithm as a robust solution, explaining its mechanism for smooth bursting and sustained traffic control. The piece also provides practical Go code examples and highlights common pitfalls in rate limiter implementation.
Read original on Dev.to #systemdesignRate limiting is a fundamental system design component essential for protecting services from abuse, misconfigurations, and traffic spikes. Without it, a sudden surge in requests can quickly overwhelm backend systems, leading to service degradation or complete outages. The article opens with a vivid anecdote of an API crashing due to an unregulated cron job, emphasizing the critical "why" behind implementing traffic control mechanisms.
The token bucket algorithm is presented as a superior alternative to simpler fixed-window counters for rate limiting. It effectively addresses the "spike" problem where fixed windows allow clients to send a large burst of requests at the boundary of a time window. The token bucket model allows requests to consume tokens from a bucket that refills at a constant rate up to a maximum capacity. This design enables services to handle short bursts of traffic while ensuring that the long-term average request rate does not exceed a defined limit.
+-------------------+
| Token Bucket |
| (capacity = C) |
+--------+----------+
^ |
tokens arrive at rate R (tokens/sec)
| |
+--------v----------+
| Refill Timer |
| (adds tokens) |
+--------+----------+
| |
request arrives -> try to take 1 token
v
+--------v----------+
| Consume? |
| if token > 0: |
| - decrement token |
| - allow request |
| else: |
| - reject/delay |
+-------------------+Common Pitfalls
Implementing a robust token bucket requires attention to details such as ensuring continuous token refill based on elapsed time (even during idle periods), using floating-point numbers for token counts to handle fractional rates accurately, and implementing proper concurrency control (like mutexes) to prevent race conditions in multi-threaded environments. Ignoring these can lead to incorrect rate limiting or even system crashes.
type TokenBucket struct {
rate float64 // tokens per second
capacity float64 // max tokens
tokens float64
lastSeen time.Time
mu sync.Mutex
}
func NewTokenBucket(rate float64, burst float64) *TokenBucket {
return &TokenBucket{
rate: rate,
capacity: burst,
tokens: burst, // start full so we can burst immediately
lastSeen: time.Now(),
}
}
// Allow checks if n tokens can be consumed.
// Returns true if the request is allowed, false otherwise.
func (b *TokenBucket) Allow(n int) bool {
b.mu.Lock()
defer b.mu.Unlock()
now := time.Now()
// refill based on elapsed time
elapsed := now.Sub(b.lastSeen).Seconds()
b.tokens += elapsed * b.rate
if b.tokens > b.capacity {
b.tokens = b.capacity
}
b.lastSeen = now
if b.tokens >= float64(n) {
b.tokens -= float64(n)
return true
}
return false
}