API Rate Limiting Patterns in Go
Implementing token bucket, sliding window, and distributed rate limiting in Go with Redis backing.
Why Rate Limiting?
Rate limiting protects your API from abuse, prevents resource exhaustion, and ensures fair usage among clients. Without it, a single misbehaving client can take down your entire service.
Token Bucket Algorithm
The token bucket is the most common rate limiting algorithm. Tokens are added to a bucket at a fixed rate, and each request consumes a token. When the bucket is empty, requests are rejected.
package ratelimit
import (
"sync"
"time"
)
type TokenBucket struct {
capacity int
tokens int
refillRate time.Duration
mu sync.Mutex
lastRefill time.Time
}
func NewTokenBucket(capacity int, refillRate time.Duration) *TokenBucket {
return &TokenBucket{
capacity: capacity,
tokens: capacity,
refillRate: refillRate,
lastRefill: time.Now(),
}
}
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
// Refill tokens based on elapsed time
now := time.Now()
elapsed := now.Sub(tb.lastRefill)
tokensToAdd := int(elapsed / tb.refillRate)
if tokensToAdd > 0 {
tb.tokens = min(tb.capacity, tb.tokens+tokensToAdd)
tb.lastRefill = now
}
// Check if request is allowed
if tb.tokens > 0 {
tb.tokens--
return true
}
return false
}Distributed Rate Limiting with Redis
For distributed systems, you need centralized rate limiting. Redis is perfect for this - it's fast and supports atomic operations:
func (r *RedisRateLimiter) Allow(ctx context.Context, key string) (bool, error) {
script := redis.NewScript(`
local tokens = redis.call('GET', KEYS[1])
if tokens == false then
tokens = ARGV[1]
end
tokens = tonumber(tokens)
if tokens > 0 then
redis.call('DECR', KEYS[1])
redis.call('EXPIRE', KEYS[1], ARGV[2])
return 1
end
return 0
`)
result, err := script.Run(ctx, r.client, []string{key}, r.capacity, r.window).Int()
if err != nil {
return false, err
}
return result == 1, nil
}Middleware Implementation
Rate limiting is typically implemented as HTTP middleware. Here's how to integrate it with a standard Go HTTP server:
func RateLimitMiddleware(limiter *RedisRateLimiter) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Use client IP as rate limit key
key := "ratelimit:" + getClientIP(r)
allowed, err := limiter.Allow(r.Context(), key)
if err != nil {
http.Error(w, "Internal Server Error", 500)
return
}
if !allowed {
w.Header().Set("Retry-After", "60")
http.Error(w, "Rate limit exceeded", 429)
return
}
next.ServeHTTP(w, r)
})
}
}Found this helpful?
I write about infrastructure, backend development, and DevOps. Follow along as I continue building.