Integration Guide: Adding Rate Limiting to Your Go Web Application
Contents
This guide walks you through integrating the go-ratelimit library into real-world Go web applications, from basic setup to production deployment.
Quick Integration
Step 1: Install the Library
go get github.com/Jouini-Mohamed-Chaker/go-ratelimit
Step 2: Basic HTTP Server Integration
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/Jouini-Mohamed-Chaker/go-ratelimit"
)
func main() {
// Create rate limiter: 1000 requests per hour with 50 burst capacity
limiter := ratelimit.NewWithConfig(ratelimit.Config{
Limit: 1000,
Window: time.Hour,
BurstSize: 50,
SkipPaths: []string{"/health", "/metrics"}, // Health checks bypass rate limiting
})
// Your existing handlers
http.HandleFunc("/api/users", handleUsers)
http.HandleFunc("/api/posts", handlePosts)
http.HandleFunc("/health", handleHealth)
// Apply rate limiting to all routes
server := &http.Server{
Addr: ":8080",
Handler: limiter.Wrap(http.DefaultServeMux),
}
log.Println("Server starting on :8080 with rate limiting (1000 req/hour)")
log.Fatal(server.ListenAndServe())
}
func handleUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"users": ["alice", "bob", "charlie"]}`)
}
func handlePosts(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"posts": [{"id": 1, "title": "Hello World"}]}`)
}
func handleHealth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
Framework-Specific Integration
Gorilla Mux
package main
import (
"encoding/json"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/Jouini-Mohamed-Chaker/go-ratelimit"
)
func main() {
// Create rate limiter with different limits for different endpoints
apiLimiter := ratelimit.NewWithConfig(ratelimit.Config{
Limit: 100, // 100 requests per minute for API
Window: time.Minute,
BurstSize: 10,
TrustedIPs: []string{"127.0.0.1"}, // Local development
})
authLimiter := ratelimit.NewWithConfig(ratelimit.Config{
Limit: 5, // Strict limit for auth endpoints
Window: time.Minute,
BurstSize: 2,
})
r := mux.NewRouter()
// API routes with standard rate limiting
apiRouter := r.PathPrefix("/api").Subrouter()
apiRouter.Use(func(next http.Handler) http.Handler {
return apiLimiter.Wrap(next)
})
apiRouter.HandleFunc("/users", getUsers).Methods("GET")
apiRouter.HandleFunc("/posts", getPosts).Methods("GET")
// Auth routes with stricter rate limiting
authRouter := r.PathPrefix("/auth").Subrouter()
authRouter.Use(func(next http.Handler) http.Handler {
return authLimiter.Wrap(next)
})
authRouter.HandleFunc("/login", handleLogin).Methods("POST")
authRouter.HandleFunc("/register", handleRegister).Methods("POST")
// Public routes without rate limiting
r.HandleFunc("/health", handleHealth).Methods("GET")
r.HandleFunc("/", handleHome).Methods("GET")
http.ListenAndServe(":8080", r)
}
func getUsers(w http.ResponseWriter, r *http.Request) {
users := []map[string]interface{}{
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"users": users})
}
func getPosts(w http.ResponseWriter, r *http.Request) {
posts := []map[string]interface{}{
{"id": 1, "title": "First Post", "author": "Alice"},
{"id": 2, "title": "Second Post", "author": "Bob"},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"posts": posts})
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
// Simulate login logic
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"message": "Login successful",
"token": "fake-jwt-token",
})
}
func handleRegister(w http.ResponseWriter, r *http.Request) {
// Simulate registration logic
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"message": "Registration successful",
})
}
func handleHealth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func handleHome(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`<h1>Welcome to My API</h1><p>Rate limiting is active!</p>`))
}
Gin Framework
package main
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/Jouini-Mohamed-Chaker/go-ratelimit"
)
// Gin middleware adapter
func RateLimitMiddleware(limiter *ratelimit.Limiter) gin.HandlerFunc {
return func(c *gin.Context) {
// Create a wrapper to make Gin's context compatible
wrapper := &ginResponseWriter{ResponseWriter: c.Writer, context: c}
if limiter.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Next()
})).ServeHTTP(wrapper, c.Request); wrapper.written {
c.Abort()
return
}
}
}
type ginResponseWriter struct {
gin.ResponseWriter
context *gin.Context
written bool
}
func (w *ginResponseWriter) Write(data []byte) (int, error) {
w.written = true
return w.ResponseWriter.Write(data)
}
func (w *ginResponseWriter) WriteHeader(statusCode int) {
if statusCode != http.StatusOK {
w.written = true
}
w.ResponseWriter.WriteHeader(statusCode)
}
func main() {
r := gin.Default()
// Create rate limiters for different route groups
apiLimiter := ratelimit.NewWithConfig(ratelimit.Config{
Limit: 200,
Window: time.Minute,
BurstSize: 20,
})
authLimiter := ratelimit.NewWithConfig(ratelimit.Config{
Limit: 10,
Window: time.Minute,
BurstSize: 3,
})
// API routes with rate limiting
api := r.Group("/api")
api.Use(RateLimitMiddleware(apiLimiter))
{
api.GET("/users", getUsers)
api.GET("/posts", getPosts)
api.POST("/posts", createPost)
}
// Auth routes with stricter limiting
auth := r.Group("/auth")
auth.Use(RateLimitMiddleware(authLimiter))
{
auth.POST("/login", login)
auth.POST("/register", register)
auth.POST("/forgot-password", forgotPassword)
}
// Public routes (no rate limiting)
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
r.GET("/", func(c *gin.Context) {
c.HTML(200, "index.html", gin.H{
"title": "My API with Rate Limiting",
})
})
r.Run(":8080")
}
func getUsers(c *gin.Context) {
c.JSON(200, gin.H{
"users": []gin.H{
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
},
})
}
func getPosts(c *gin.Context) {
c.JSON(200, gin.H{
"posts": []gin.H{
{"id": 1, "title": "First Post"},
{"id": 2, "title": "Second Post"},
},
})
}
func createPost(c *gin.Context) {
c.JSON(201, gin.H{
"message": "Post created",
"id": 123,
})
}
func login(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Login successful",
"token": "fake-jwt-token",
})
}
func register(c *gin.Context) {
c.JSON(201, gin.H{
"message": "Registration successful",
})
}
func forgotPassword(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Password reset email sent",
})
}
Echo Framework
package main
import (
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/Jouini-Mohamed-Chaker/go-ratelimit"
)
// Echo middleware adapter
func RateLimitMiddleware(limiter *ratelimit.Limiter) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
req := c.Request()
res := c.Response().Writer
// Check if request should be rate limited
handler := limiter.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Continue to next handler
if err := next(c); err != nil {
c.Error(err)
}
}))
handler.ServeHTTP(res, req)
return nil
}
}
}
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Rate limiters
apiLimiter := ratelimit.NewWithConfig(ratelimit.Config{
Limit: 500,
Window: time.Hour,
BurstSize: 50,
SkipPaths: []string{"/health", "/metrics"},
})
strictLimiter := ratelimit.NewWithConfig(ratelimit.Config{
Limit: 15,
Window: time.Minute,
BurstSize: 5,
})
// API group with rate limiting
api := e.Group("/api", RateLimitMiddleware(apiLimiter))
api.GET("/users", getUsers)
api.GET("/posts", getPosts)
api.POST("/posts", createPost)
// Auth group with strict rate limiting
auth := e.Group("/auth", RateLimitMiddleware(strictLimiter))
auth.POST("/login", login)
auth.POST("/register", register)
// Public routes
e.GET("/health", health)
e.GET("/", home)
e.Logger.Fatal(e.Start(":8080"))
}
func getUsers(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{
"users": []map[string]interface{}{
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
},
})
}
func getPosts(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{
"posts": []map[string]interface{}{
{"id": 1, "title": "Hello World"},
},
})
}
func createPost(c echo.Context) error {
return c.JSON(http.StatusCreated, map[string]string{
"message": "Post created successfully",
})
}
func login(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "Login successful",
"token": "fake-jwt-token",
})
}
func register(c echo.Context) error {
return c.JSON(http.StatusCreated, map[string]string{
"message": "Registration successful",
})
}
func health(c echo.Context) error {
return c.String(http.StatusOK, "OK")
}
func home(c echo.Context) error {
return c.HTML(http.StatusOK, "<h1>Welcome to My API</h1><p>Rate limiting active!</p>")
}
Production Configuration
Environment-Based Configuration
package main
import (
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/Jouini-Mohamed-Chaker/go-ratelimit"
)
type AppConfig struct {
Port string
RateLimit int
RateWindow time.Duration
BurstSize int
TrustedIPs []string
SkipPaths []string
EnableRateLimit bool
}
func loadConfig() *AppConfig {
config := &AppConfig{
Port: getEnv("PORT", "8080"),
RateLimit: getEnvInt("RATE_LIMIT", 1000),
RateWindow: getEnvDuration("RATE_WINDOW", "1h"),
BurstSize: getEnvInt("BURST_SIZE", 100),
TrustedIPs: getEnvSlice("TRUSTED_IPS", []string{"127.0.0.1"}),
SkipPaths: getEnvSlice("SKIP_PATHS", []string{"/health", "/metrics"}),
EnableRateLimit: getEnvBool("ENABLE_RATE_LIMIT", true),
}
return config
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}
func getEnvDuration(key string, defaultValue string) time.Duration {
value := getEnv(key, defaultValue)
if duration, err := time.ParseDuration(value); err == nil {
return duration
}
// Fallback parsing
if duration, err := time.ParseDuration(defaultValue); err == nil {
return duration
}
return time.Hour
}
func getEnvBool(key string, defaultValue bool) bool {
if value := os.Getenv(key); value != "" {
if boolValue, err := strconv.ParseBool(value); err == nil {
return boolValue
}
}
return defaultValue
}
func getEnvSlice(key string, defaultValue []string) []string {
if value := os.Getenv(key); value != "" {
return strings.Split(value, ",")
}
return defaultValue
}
func main() {
config := loadConfig()
var handler http.Handler = http.DefaultServeMux
// Setup your routes
http.HandleFunc("/api/users", handleAPI)
http.HandleFunc("/api/posts", handleAPI)
http.HandleFunc("/health", handleHealth)
// Apply rate limiting if enabled
if config.EnableRateLimit {
limiter := ratelimit.NewWithConfig(ratelimit.Config{
Limit: config.RateLimit,
Window: config.RateWindow,
BurstSize: config.BurstSize,
SkipPaths: config.SkipPaths,
TrustedIPs: config.TrustedIPs,
})
// Custom rate limit handler for better API responses
limiter.OnLimitReached = func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Retry-After", "60")
w.WriteHeader(http.StatusTooManyRequests)
w.Write([]byte(`{
"error": "rate_limit_exceeded",
"message": "Too many requests. Please slow down.",
"retry_after": 60
}`))
}
handler = limiter.Wrap(handler)
log.Printf("Rate limiting enabled: %d requests per %v (burst: %d)",
config.RateLimit, config.RateWindow, config.BurstSize)
} else {
log.Println("Rate limiting disabled")
}
log.Printf("Server starting on port %s", config.Port)
log.Fatal(http.ListenAndServe(":"+config.Port, handler))
}
func handleAPI(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message": "API response", "path": "` + r.URL.Path + `"}`))
}
func handleHealth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
Docker Configuration
Create a .env file:
PORT=8080
RATE_LIMIT=2000
RATE_WINDOW=1h
BURST_SIZE=200
TRUSTED_IPS=127.0.0.1,10.0.0.0/8,192.168.0.0/16
SKIP_PATHS=/health,/metrics,/favicon.ico
ENABLE_RATE_LIMIT=true
Dockerfile:
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
docker-compose.yml:
version: '3.8'
services:
api:
build: .
ports:
- "8080:8080"
env_file:
- .env
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
Advanced Patterns
Per-User Rate Limiting
package main
import (
"crypto/sha256"
"fmt"
"net/http"
"sync"
"time"
"github.com/Jouini-Mohamed-Chaker/go-ratelimit"
)
type UserLimiterManager struct {
limiters map[string]*ratelimit.Limiter
mu sync.RWMutex
config ratelimit.Config
}
func NewUserLimiterManager(config ratelimit.Config) *UserLimiterManager {
return &UserLimiterManager{
limiters: make(map[string]*ratelimit.Limiter),
config: config,
}
}
func (m *UserLimiterManager) GetLimiter(userID string) *ratelimit.Limiter {
m.mu.RLock()
limiter, exists := m.limiters[userID]
m.mu.RUnlock()
if exists {
return limiter
}
// Create new limiter for user
m.mu.Lock()
defer m.mu.Unlock()
// Double-check after acquiring write lock
if limiter, exists := m.limiters[userID]; exists {
return limiter
}
limiter = ratelimit.NewWithConfig(m.config)
m.limiters[userID] = limiter
return limiter
}
// Cleanup old limiters periodically (optional)
func (m *UserLimiterManager) Cleanup() {
m.mu.Lock()
defer m.mu.Unlock()
for userID, limiter := range m.limiters {
// Remove limiters that haven't been used recently
// This is a simple implementation - you might want more sophisticated cleanup
if limiter.RemainingTokens() == float64(m.config.Limit) {
delete(m.limiters, userID)
}
}
}
func getUserIDFromRequest(r *http.Request) string {
// Try to get user ID from various sources
if userID := r.Header.Get("X-User-ID"); userID != "" {
return userID
}
if userID := r.URL.Query().Get("user_id"); userID != "" {
return userID
}
// Fall back to IP-based identification
clientIP := getClientIP(r)
hash := sha256.Sum256([]byte(clientIP))
return fmt.Sprintf("ip-%x", hash[:8]) // Use first 8 bytes of hash
}
func getClientIP(r *http.Request) string {
if ip := r.Header.Get("X-Forwarded-For"); ip != "" {
return ip
}
if ip := r.Header.Get("X-Real-IP"); ip != "" {
return ip
}
return r.RemoteAddr
}
func main() {
// Create per-user rate limiter manager
userManager := NewUserLimiterManager(ratelimit.Config{
Limit: 100, // 100 requests per user
Window: time.Hour, // per hour
BurstSize: 10,
})
// Global rate limiter for overall protection
globalLimiter := ratelimit.NewWithConfig(ratelimit.Config{
Limit: 10000, // 10k total requests
Window: time.Hour, // per hour
BurstSize: 1000,
SkipPaths: []string{"/health"},
})
// Start cleanup goroutine
go func() {
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for range ticker.C {
userManager.Cleanup()
}
}()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// First check global rate limit
if !globalLimiter.Allow() {
http.Error(w, "Global rate limit exceeded", http.StatusTooManyRequests)
return
}
// Then check per-user rate limit
userID := getUserIDFromRequest(r)
userLimiter := userManager.GetLimiter(userID)
if !userLimiter.Allow() {
w.Header().Set("X-User-ID", userID)
http.Error(w, "User rate limit exceeded", http.StatusTooManyRequests)
return
}
// Add headers for debugging
w.Header().Set("X-User-ID", userID)
w.Header().Set("X-Remaining-Global", fmt.Sprintf("%.0f", globalLimiter.RemainingTokens()))
w.Header().Set("X-Remaining-User", fmt.Sprintf("%.0f", userLimiter.RemainingTokens()))
// Your API logic here
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"message": "Success", "user_id": "%s"}`, userID)
})
http.Handle("/api/", handler)
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
log.Println("Server starting with per-user rate limiting...")
http.ListenAndServe(":8080", nil)
}
Monitoring and Observability
Metrics Integration
package main
import (
"expvar"
"fmt"
"net/http"
"time"
"github.com/Jouini-Mohamed-Chaker/go-ratelimit"
)
var (
rateLimitStats = expvar.NewMap("ratelimit")
)
func setupMetrics(limiter *ratelimit.Limiter) {
// Update metrics every 10 seconds
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
total, allowed, blocked := limiter.GetStats()
remaining := limiter.RemainingTokens()
rateLimitStats.Set("total_requests", &expvar.Int{})
rateLimitStats.Get("total_requests").(*expvar.Int).Set(total)
rateLimitStats.Set("allowed_requests", &expvar.Int{})
rateLimitStats.Get("allowed_requests").(*expvar.Int).Set(allowed)
rateLimitStats.Set("blocked_requests", &expvar.Int{})
rateLimitStats.Get("blocked_requests").(*expvar.Int).Set(blocked)
rateLimitStats.Set("remaining_tokens", &expvar.Float{})
rateLimitStats.Get("remaining_tokens").(*expvar.Float).Set(remaining)
if total > 0 {
blockRate := float64(blocked) / float64(total) * 100
rateLimitStats.Set("block_rate_percent", &expvar.Float{})
rateLimitStats.Get("block_rate_percent").(*expvar.Float).Set(blockRate)
}
}
}()
}
func main() {
limiter := ratelimit.NewWithConfig(ratelimit.Config{
Limit: 100,
Window: time.Minute,
BurstSize: 10,
SkipPaths: []string{"/health", "/metrics", "/debug/vars"},
})
// Setup metrics collection
setupMetrics(limiter)
// Add custom callbacks for real-time monitoring
limiter.OnLimitReached = func(w http.ResponseWriter, r *http.Request) {
// Log rate limit events
fmt.Printf("[RATE_LIMIT] %s %s from %s\n",
r.Method, r.URL.Path, r.RemoteAddr)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
w.Write([]byte(`{
"error": "rate_limit_exceeded",
"message": "Please slow down your requests",
"retry_after": 60
}`))
}
limiter.OnAllow = func(r *http.Request, remaining float64) {
// Log when running low on tokens
if remaining < 10 {
fmt.Printf("[RATE_LIMIT] Low tokens: %.0f remaining for %s\n",
remaining, r.RemoteAddr)
}
}
// Your API routes
http.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"data": "Hello World", "timestamp": "%s"}`,
time.Now().Format(time.RFC3339))
})
// Metrics endpoint (expvar provides /debug/vars automatically)
http.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
total, allowed, blocked := limiter.GetStats()
remaining := limiter.RemainingTokens()
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{
"rate_limit": {
"total_requests": %d,
"allowed_requests": %d,
"blocked_requests": %d,
"remaining_tokens": %.2f,
"block_rate_percent": %.2f
}
}`, total, allowed, blocked, remaining,
func() float64 {
if total > 0 {
return float64(blocked) / float64(total) * 100
}
return 0
}())
})
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
// Apply rate limiting
server := &http.Server{
Addr: ":8080",
Handler: limiter.Wrap(http.DefaultServeMux),
}
fmt.Println("Server starting on :8080")
fmt.Println("Metrics available at /metrics and /debug/vars")
server.ListenAndServe()
}