How to Implement Rate Limiting to Prevent Abuse

Implement rate limiting in Go is best done using the `golang.org/x/time/rate` package, which provides a token bucket algorithm to control the frequency of operations.

Implement rate limiting in Go is best done using the golang.org/x/time/rate package, which provides a token bucket algorithm to control the frequency of operations. You create a *rate.Limiter with a specific burst capacity and refill rate, then check if a request can proceed using Allow() or wait for a token using Wait().

Here is a practical example using the token bucket approach to limit API requests per client IP:

package main

import (
	"fmt"
	"net/http"
	"sync"
	"time"

	"golang.org/x/time/rate"
)

// RateLimiterMap stores a limiter per client IP
var (
	limiterMap = make(map[string]*rate.Limiter)
	mu         sync.RWMutex
)

// NewRateLimiter creates a limiter allowing 5 requests per second with a burst of 10
func NewRateLimiter() *rate.Limiter {
	return rate.NewLimiter(rate.Every(200*time.Millisecond), 10)
}

func rateLimitMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		clientIP := r.RemoteAddr
		mu.Lock()
		limiter, exists := limiterMap[clientIP]
		if !exists {
			limiter = NewRateLimiter()
			limiterMap[clientIP] = limiter
		}
		mu.Unlock()

		if !limiter.Allow() {
			http.Error(w, "Too many requests", http.StatusTooManyRequests)
			return
		}

		next.ServeHTTP(w, r)
	})
}

func main() {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Request processed successfully")
	})

	http.ListenAndServe(":8080", rateLimitMiddleware(handler))
}

For scenarios where you need to block the request until a token is available rather than rejecting it immediately, use Wait() instead of Allow(). This is useful for background jobs or queue processing where dropping requests is not an option.

// Example: Blocking until a token is available
func processWithWait(limiter *rate.Limiter, task func()) {
	// This blocks until a token is available or context is cancelled
	if err := limiter.Wait(context.Background()); err != nil {
		fmt.Println("Context cancelled while waiting for token")
		return
	}
	task()
}

Key considerations for production use include cleaning up the limiterMap to prevent memory leaks from stale IPs, using a distributed store like Redis if running behind a load balancer, and setting appropriate burst limits based on your infrastructure capacity. The token bucket algorithm is preferred over simple counters because it allows for short bursts of traffic while maintaining an average rate limit over time.