Introduction
Rate limiting protects your API from abuse, prevents resource exhaustion, and ensures fair access for all clients. Go's extended library provides a token bucket rate limiter out of the box.
Key Concepts
- Token bucket algorithm: A rate limiting strategy where tokens are added at a fixed rate. Each request consumes a token; when the bucket is empty, requests are rejected.
- rate.NewLimiter(rate, burst): Creates a limiter allowing
raterequests per second with aburstsize for temporary spikes. - Per-IP limiting: Maintaining a separate rate limiter for each client IP address to prevent one client from consuming the entire quota.
- HTTP 429 Too Many Requests: The standard status code returned when a rate limit is exceeded.
Real World Context
Without rate limiting, a single misbehaving client (or attacker) can overwhelm your API, causing downtime for everyone. Rate limiting is a required layer for any public-facing API and a best practice for internal services.
Deep Dive
A global rate limiter using Go's x/time/rate package.
goimport "golang.org/x/time/rate" var limiter = rate.NewLimiter(rate.Limit(10), 30) // 10 req/sec, burst 30 func rateLimitMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !limiter.Allow() { http.Error(w, "Too Many Requests", http.StatusTooManyRequests) return } next.ServeHTTP(w, r) }) }
limiter.Allow() returns false when the token bucket is empty, triggering a 429 response.
For per-IP rate limiting, maintain a map of limiters keyed by client IP.
gotype ipLimiter struct { limiters map[string]*rate.Limiter mu sync.Mutex } func (i *ipLimiter) getLimiter(ip string) *rate.Limiter { i.mu.Lock() defer i.mu.Unlock() if limiter, ok := i.limiters[ip]; ok { return limiter } limiter := rate.NewLimiter(rate.Limit(5), 10) i.limiters[ip] = limiter return limiter }
This gives each IP its own bucket, so one abusive client does not affect others.
Common Pitfalls
- Using a global limiter for all clients — One client can consume the entire rate limit, blocking legitimate users. Use per-IP or per-API-key limiters.
- Not cleaning up stale limiters — The per-IP map grows unbounded. Add a background goroutine to evict entries that have not been used recently.
Best Practices
- Include a
Retry-Afterheader — Tell rate-limited clients when they can retry, improving their experience and reducing retry storms. - Use different limits for different endpoints — Auth endpoints need stricter limits than read-only endpoints.
Summary
- Use
golang.org/x/time/ratefor token bucket rate limiting in Go. - Per-IP limiters prevent one client from monopolizing the API.
- Return HTTP 429 with a
Retry-Afterheader when rejecting rate-limited requests.
Code Examples
go
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := getClientIP(r)
limiter := rl.getLimiter(ip)
if !limiter.Allow() {
w.Header().Set("Retry-After", "60")
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}