IP-Based Rate Limiter Middleware for HTTP Servers
HTTP middleware that rate-limits requests per client IP using a token bucket and returns 429 with Retry-After hints.
main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
package main
import (
"fmt"
"net/http"
"sync"
"time"
"golang.org/x/time/rate"
)
// IPRateLimiter manages rate limiters for multiple IP addresses
type IPRateLimiter struct {
mu sync.RWMutex
limiters map[string]*rate.Limiter
r rate.Limit
burst int
}
// NewIPRateLimiter creates a new IP-based rate limiter
func NewIPRateLimiter(r rate.Limit, burst int) *IPRateLimiter {
return &IPRateLimiter{
limiters: make(map[string]*rate.Limiter),
r: r,
burst: burst,
}
}
// AddIP creates a new rate limiter for an IP address if it doesn't exist
func (i *IPRateLimiter) AddIP(ip string) *rate.Limiter {
i.mu.Lock()
defer i.mu.Unlock()
limiter, exists := i.limiters[ip]
if !exists {
limiter = rate.NewLimiter(i.r, i.burst)
i.limiters[ip] = limiter
}
return limiter
}
// GetLimiter retrieves the rate limiter for an IP address (creates if missing)
func (i *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {
i.mu.RLock()
limiter, exists := i.limiters[ip]
i.mu.RUnlock()
if exists {
return limiter
}
// If limiter doesn't exist, create it
return i.AddIP(ip)
}
// CleanupStale removes rate limiters for IPs that haven't been used in the specified duration
func (i *IPRateLimiter) CleanupStale(threshold time.Duration) {
// Note: For production use, track last access time per IP and remove stale entries
// This is a simplified version that clears all entries (adjust as needed)
i.mu.Lock()
i.limiters = make(map[string]*rate.Limiter)
i.mu.Unlock()
}
// RateLimitMiddleware creates an HTTP middleware that enforces rate limits
func RateLimitMiddleware(limiter *IPRateLimiter) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get client IP address
clientIP := r.RemoteAddr
// For production, use X-Forwarded-For or X-Real-IP headers if behind a proxy
// clientIP := r.Header.Get("X-Forwarded-For")
// if clientIP == "" { clientIP = r.RemoteAddr }
// Get rate limiter for this IP
ipLimiter := limiter.GetLimiter(clientIP)
// Check if request is allowed
if !ipLimiter.Allow() {
// Calculate retry after time (approximate)
retryAfter := time.Duration(float64(limiter.burst)/float64(limiter.r)) * time.Second
// Set Retry-After header
w.Header().Set("Retry-After", fmt.Sprintf("%d", int(retryAfter.Seconds())))
// Return 429 Too Many Requests
http.Error(w, fmt.Sprintf("rate limit exceeded - retry after %v", retryAfter), http.StatusTooManyRequests)
return
}
// Pass request to next handler
next.ServeHTTP(w, r)
})
}
}
// Example handler
func helloHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, Rate Limited World!"))
}
func main() {
// Create rate limiter: 10 requests per second, burst of 5
limiter := NewIPRateLimiter(10, 5)
// Start background cleanup every hour (remove stale limiters)
go func() {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for range ticker.C {
limiter.CleanupStale(1 * time.Hour)
fmt.Println("Cleaned up stale rate limiters")
}
}()
// Create HTTP server with rate limit middleware
handler := RateLimitMiddleware(limiter)(http.HandlerFunc(helloHandler))
http.Handle("/", handler)
// Start server
fmt.Println("Server starting on :8080 - Rate limit: 10 req/sec, burst: 5")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Printf("Server failed to start: %v\n", err)
}
}How It Works
HTTP middleware that rate-limits requests per client IP using a token bucket and returns 429 with Retry-After hints.
Maintains a map of limiters keyed by IP, refills tokens over time, checks Allow on each request, and writes appropriate responses or forwards to the next handler.
Key Concepts
- 1Per-IP limiter instances isolate abusive clients.
- 2Token bucket math allows short bursts while enforcing sustained limits.
- 3Retry-After header communicates when to retry.
When to Use This Pattern
- Protecting public APIs from scraping or brute force.
- Guarding login endpoints to slow credential stuffing.
- Rate limiting webhooks from noisy partners.
Best Practices
- Normalize or forward the correct client IP when behind proxies.
- Persist counters if you need limits across multiple instances.
- Log rejections for observability and tuning.
Go Version1.19
Difficultyintermediate
Production ReadyYes
Lines of Code127