Rate Limiting with Token Bucket Algorithm
Controls how many operations can run per interval by consuming and refilling tokens at a steady pace.
main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
)
// TokenBucket implements a thread-safe token bucket rate limiter
type TokenBucket struct {
mu sync.Mutex
capacity int // Maximum number of tokens
tokens int // Current number of tokens
refillRate int // Tokens to add per refill interval
refillPeriod time.Duration // Interval for refilling tokens
lastRefill time.Time // Timestamp of last refill
}
// NewTokenBucket creates a new TokenBucket with specified capacity and refill rate
func NewTokenBucket(capacity, refillRate int, refillPeriod time.Duration) *TokenBucket {
return &TokenBucket{
capacity: capacity,
tokens: capacity, // Start with full bucket
refillRate: refillRate,
refillPeriod: refillPeriod,
lastRefill: time.Now(),
}
}
// refill adds tokens based on time elapsed since last refill
func (tb *TokenBucket) refill() {
tb.mu.Lock()
defer tb.mu.Unlock()
now := time.Now()
elapsed := now.Sub(tb.lastRefill)
if elapsed < tb.refillPeriod {
return
}
// Calculate number of refills to perform
refills := int(elapsed / tb.refillPeriod)
newTokens := refills * tb.refillRate
tb.tokens = min(tb.tokens+newTokens, tb.capacity)
tb.lastRefill = now.Add(-(elapsed % tb.refillPeriod)) // Preserve leftover time
}
// Allow checks if a token is available (blocks until token is available or context expires)
func (tb *TokenBucket) Allow(ctx context.Context) bool {
for {
tb.refill()
tb.mu.Lock()
if tb.tokens > 0 {
tb.tokens--
tb.mu.Unlock()
return true
}
tb.mu.Unlock()
// Check if context is done before waiting
select {
case <-ctx.Done():
return false
case <-time.After(100 * time.Millisecond): // Short sleep to prevent busy waiting
continue
}
}
}
// Example usage
func main() {
// Create rate limiter: 5 tokens capacity, 1 token per second refill rate
limiter := NewTokenBucket(5, 1, 1*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var wg sync.WaitGroup
// Simulate 10 concurrent requests
for i := 0; i < 10; i++ {
wg.Add(1)
go func(requestID int) {
defer wg.Done()
start := time.Now()
if limiter.Allow(ctx) {
log.Printf("Request %d: allowed (waited %v)", requestID, time.Since(start))
} else {
log.Printf("Request %d: denied (context expired)", requestID)
}
}(i)
}
wg.Wait()
fmt.Println("All requests processed")
}How It Works
Controls how many operations can run per interval by consuming and refilling tokens at a steady pace.
Limiter stores capacity, refill rate, and last refill timestamp; a mutex protects shared counters; Allow refills tokens based on elapsed time then decrements if available; main demonstrates gating simulated work across goroutines.
Key Concepts
- 1Refill logic is time-based so bursts deplete and slowly recover.
- 2Mutex guards token calculations for concurrent callers.
- 3Supports both single operation checks and higher-level middleware usage.
- 4Capacity and rate are configurable to tune throughput.
When to Use This Pattern
- Protecting APIs or database connections from spikes.
- Throttling background jobs or cron tasks.
- Client-side rate limiting before hitting third-party services.
- Ensuring fairness among worker goroutines sharing a resource.
Best Practices
- Pick capacity to allow small bursts without overwhelming backends.
- Record rejection metrics to tune limits over time.
- Share one limiter per resource rather than per request.
- Guard state with a mutex; atomics alone can miss refill math.
Go Version1.13
Difficultyintermediate
Production ReadyYes
Lines of Code99