Skip to content

Latest commit

 

History

History
582 lines (439 loc) · 13.7 KB

File metadata and controls

582 lines (439 loc) · 13.7 KB

API Documentation

Complete API reference for the Go Rate Limiter library.

Table of Contents

Types

Config

Configuration structure for creating a rate limiter with advanced options.

type Config struct {
    Limit      int           // Maximum number of requests allowed in the window
    Window     time.Duration // Time window for the rate limit
    BurstSize  int           // Additional tokens for burst traffic (optional)
    SkipPaths  []string      // HTTP paths to skip rate limiting (optional)
    TrustedIPs []string      // IP addresses to skip rate limiting (optional)
}

Fields:

  • Limit: The maximum number of requests allowed within the specified window
  • Window: Duration of the rate limiting window (e.g., time.Minute, time.Hour)
  • BurstSize: Additional tokens added to handle traffic bursts (default: 0)
  • SkipPaths: Slice of URL paths that bypass rate limiting
  • TrustedIPs: Slice of IP addresses that bypass rate limiting

Limiter

The main rate limiter struct that implements the token bucket algorithm.

type Limiter struct {
    // Callback functions
    OnLimitReached func(w http.ResponseWriter, r *http.Request)
    OnAllow        func(r *http.Request, remainingTokens float64)
    // ... other internal fields
}

Public Fields:

  • OnLimitReached: Called when rate limit is exceeded (HTTP requests only)
  • OnAllow: Called when a request is allowed (HTTP requests only)

Stats

Statistics tracking for the rate limiter.

type Stats struct {
    TotalRequests   int64
    AllowedRequests int64
    BlockedRequests int64
    // ... internal mutex
}

Functions

New

Creates a new rate limiter with basic configuration.

func New(limit int, window time.Duration) *Limiter

Parameters:

  • limit: Maximum number of requests allowed
  • window: Time window duration

Returns: *Limiter

Example:

// 100 requests per minute
limiter := ratelimit.New(100, time.Minute)

NewWithConfig

Creates a new rate limiter with advanced configuration options.

func NewWithConfig(config Config) *Limiter

Parameters:

  • config: Configuration struct with all options

Returns: *Limiter

Example:

config := ratelimit.Config{
    Limit:      1000,
    Window:     time.Hour,
    BurstSize:  100,
    SkipPaths:  []string{"/health", "/ping"},
    TrustedIPs: []string{"127.0.0.1", "10.0.0.0/8"},
}
limiter := ratelimit.NewWithConfig(config)

Methods

Allow

Checks if a single request should be allowed.

func (l *Limiter) Allow() bool

Returns: bool - true if request is allowed, false if rate limited

Example:

if limiter.Allow() {
    // Process the request
    processRequest()
} else {
    // Handle rate limit exceeded
    handleRateLimit()
}

AllowN

Checks if N requests should be allowed simultaneously.

func (l *Limiter) AllowN(n int) bool

Parameters:

  • n: Number of requests to check

Returns: bool - true if all N requests are allowed

Example:

// Check if we can process a batch of 5 requests
if limiter.AllowN(5) {
    processBatchRequests(5)
} else {
    handleRateLimit()
}

Wait

Blocks until a request can be allowed or the context is cancelled.

func (l *Limiter) Wait(ctx context.Context) error

Parameters:

  • ctx: Context for cancellation and timeout

Returns: error - Context error if cancelled/timed out, nil on success

Example:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := limiter.Wait(ctx); err != nil {
    if err == context.DeadlineExceeded {
        handleTimeout()
    } else {
        handleCancellation()
    }
} else {
    // Request is now allowed
    processRequest()
}

RemainingTokens

Returns the current number of available tokens.

func (l *Limiter) RemainingTokens() float64

Returns: float64 - Number of available tokens (can be fractional)

Example:

remaining := limiter.RemainingTokens()
fmt.Printf("Available tokens: %.2f\n", remaining)

// Check if we have enough tokens before making a request
if remaining >= 5 {
    processBatchRequests(5)
}

Reset

Resets the limiter to its initial state with maximum tokens.

func (l *Limiter) Reset()

Example:

// Reset limiter (useful for testing or administrative actions)
limiter.Reset()

GetStats

Returns current statistics for the rate limiter.

func (l *Limiter) GetStats() (total, allowed, blocked int64)

Returns:

  • total: Total number of requests processed
  • allowed: Number of requests that were allowed
  • blocked: Number of requests that were blocked

Example:

total, allowed, blocked := limiter.GetStats()
fmt.Printf("Stats - Total: %d, Allowed: %d, Blocked: %d\n", 
    total, allowed, blocked)

// Calculate success rate
if total > 0 {
    successRate := float64(allowed) / float64(total) * 100
    fmt.Printf("Success rate: %.2f%%\n", successRate)
}

Wrap

Returns HTTP middleware that applies rate limiting.

func (l *Limiter) Wrap(next http.Handler) http.Handler

Parameters:

  • next: The HTTP handler to wrap

Returns: http.Handler - Wrapped handler with rate limiting

Example:

mux := http.NewServeMux()
mux.HandleFunc("/api", apiHandler)

// Apply rate limiting to all routes
http.ListenAndServe(":8080", limiter.Wrap(mux))

WrapFunc

Returns HTTP middleware as a function for use with HandlerFunc.

func (l *Limiter) WrapFunc(next http.HandlerFunc) http.HandlerFunc

Parameters:

  • next: The HTTP handler function to wrap

Returns: http.HandlerFunc - Wrapped handler function

Example:

http.HandleFunc("/api", limiter.WrapFunc(apiHandler))

SetSkipPaths

Updates the paths that should skip rate limiting.

func (l *Limiter) SetSkipPaths(paths []string)

Parameters:

  • paths: Slice of URL paths to skip

Example:

// Update skip paths at runtime
limiter.SetSkipPaths([]string{"/health", "/metrics", "/admin/status"})

SetTrustedIPs

Updates the IP addresses that should skip rate limiting.

func (l *Limiter) SetTrustedIPs(ips []string)

Parameters:

  • ips: Slice of IP addresses to trust

Example:

// Update trusted IPs at runtime
limiter.SetTrustedIPs([]string{"127.0.0.1", "10.0.0.1", "192.168.1.100"})

Examples

Basic Web Server with Rate Limiting

package main

import (
    "fmt"
    "net/http"
    "time"
    
    "github.com/yourusername/ratelimit"
)

func main() {
    // Create rate limiter: 60 requests per minute
    limiter := ratelimit.New(60, time.Minute)
    
    // API handler
    apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintf(w, `{"message": "API response", "timestamp": "%s"}`, 
            time.Now().Format(time.RFC3339))
    })
    
    // Apply rate limiting
    http.Handle("/api", limiter.Wrap(apiHandler))
    
    fmt.Println("Server starting on :8080 (60 requests/minute limit)")
    http.ListenAndServe(":8080", nil)
}

Advanced Configuration with Custom Callbacks

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "time"
    
    "github.com/yourusername/ratelimit"
)

func main() {
    // Advanced configuration
    config := ratelimit.Config{
        Limit:      100,              // 100 requests
        Window:     time.Minute,      // per minute
        BurstSize:  20,               // allow bursts of 20
        SkipPaths:  []string{"/health", "/ping"},
        TrustedIPs: []string{"127.0.0.1"},
    }
    
    limiter := ratelimit.NewWithConfig(config)
    
    // Custom rate limit exceeded handler
    limiter.OnLimitReached = func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusTooManyRequests)
        
        response := map[string]interface{}{
            "error":      "Rate limit exceeded",
            "message":    "Please slow down your requests",
            "retry_after": 60,
        }
        
        json.NewEncoder(w).Encode(response)
    }
    
    // Custom allow callback for logging
    limiter.OnAllow = func(r *http.Request, remaining float64) {
        log.Printf("Request from %s allowed, remaining: %.0f", 
            r.RemoteAddr, remaining)
    }
    
    // Handlers
    http.HandleFunc("/api", limiter.WrapFunc(apiHandler))
    http.HandleFunc("/health", healthHandler) // This will skip rate limiting
    http.HandleFunc("/stats", limiter.WrapFunc(statsHandler))
    
    log.Println("Server starting on :8080")
    http.ListenAndServe(":8080", nil)
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{
        "message": "Hello from API",
        "time":    time.Now().Format(time.RFC3339),
    })
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}

func statsHandler(w http.ResponseWriter, r *http.Request) {
    // This would need access to the limiter instance
    // Implementation depends on your application structure
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{
        "status": "Stats endpoint",
    })
}

Using the Limiter Programmatically

package main

import (
    "context"
    "fmt"
    "sync"
    "time"
    
    "github.com/yourusername/ratelimit"
)

func main() {
    // Create a rate limiter: 5 requests per second
    limiter := ratelimit.New(5, time.Second)
    
    // Simulate multiple goroutines making requests
    var wg sync.WaitGroup
    
    // Start 10 workers
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(workerID int) {
            defer wg.Done()
            
            for j := 0; j < 5; j++ {
                if limiter.Allow() {
                    fmt.Printf("Worker %d: Request %d allowed\n", workerID, j+1)
                    // Simulate work
                    time.Sleep(100 * time.Millisecond)
                } else {
                    fmt.Printf("Worker %d: Request %d rate limited\n", workerID, j+1)
                    
                    // Wait for permission with timeout
                    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
                    if err := limiter.Wait(ctx); err != nil {
                        fmt.Printf("Worker %d: Failed to wait: %v\n", workerID, err)
                    } else {
                        fmt.Printf("Worker %d: Request %d allowed after waiting\n", workerID, j+1)
                    }
                    cancel()
                }
            }
        }(i)
    }
    
    // Monitor stats
    go func() {
        ticker := time.NewTicker(2 * time.Second)
        defer ticker.Stop()
        
        for range ticker.C {
            total, allowed, blocked := limiter.GetStats()
            remaining := limiter.RemainingTokens()
            
            fmt.Printf("\nStats: Total=%d, Allowed=%d, Blocked=%d, Remaining=%.2f\n\n", 
                total, allowed, blocked, remaining)
            
            if total >= 50 { // Stop monitoring after 50 total requests
                return
            }
        }
    }()
    
    wg.Wait()
    
    // Final stats
    total, allowed, blocked := limiter.GetStats()
    fmt.Printf("\nFinal Stats: Total=%d, Allowed=%d, Blocked=%d\n", 
        total, allowed, blocked)
}

Best Practices

Choosing Rate Limits

  • API Endpoints: Start with 1000 requests/hour for general API usage
  • Authentication: Use stricter limits (e.g., 5 attempts/minute) for login endpoints
  • Heavy Operations: Limit expensive operations to 10-100 requests/hour
  • Burst Traffic: Set BurstSize to 10-20% of your limit for handling spikes

Performance Optimization

  • Per-User Limiting: Create separate limiter instances for different users/clients
  • Memory Usage: Consider using a limiter pool for high-traffic applications
  • Statistics: Only call GetStats() when needed, as it requires a lock

Error Handling

// Always handle context cancellation properly
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := limiter.Wait(ctx); err != nil {
    switch err {
    case context.DeadlineExceeded:
        // Handle timeout
        return fmt.Errorf("rate limiter timeout")
    case context.Canceled:
        // Handle cancellation
        return fmt.Errorf("request canceled")
    default:
        return fmt.Errorf("unexpected error: %w", err)
    }
}

HTTP Headers

When using the HTTP middleware, the following headers are automatically set:

  • X-RateLimit-Limit: Maximum number of requests allowed
  • X-RateLimit-Remaining: Number of requests remaining in the current window
  • Retry-After: Seconds to wait before retrying (only when rate limited)

Thread Safety

The rate limiter is fully thread-safe and can be used concurrently from multiple goroutines. All methods use appropriate locking mechanisms.

Testing

// Reset limiter between tests
func TestRateLimiter(t *testing.T) {
    limiter := ratelimit.New(5, time.Second)
    
    // Test allowing requests
    for i := 0; i < 5; i++ {
        if !limiter.Allow() {
            t.Errorf("Request %d should be allowed", i)
        }
    }
    
    // This should be rate limited
    if limiter.Allow() {
        t.Error("Request should be rate limited")
    }
    
    // Reset for next test
    limiter.Reset()
}