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