Effective Rate Limiting in Go: Strategies and Examples

48 views

Implementing rate limiting in a Go application can be crucial for controlling access to resources, preventing abuse, and ensuring fair usage. Here's a guide on how to set up rate limiting in Go, with examples using different methods and libraries.

Using golang.org/x/time/rate Package

The golang.org/x/time/rate package is a standard and straightforward way to implement rate limiting in Go.

Example

package main

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

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

var rateLimiter *rate.Limiter

func init() {
    // Limit to 5 requests per second
    rateLimiter = rate.NewLimiter(5, 5)
}

func rateLimitedHandler(w http.ResponseWriter, r *http.Request) {
    if !rateLimiter.Allow() {
        http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
        return
    }

    // Simulate work
    fmt.Fprintln(w, "Request successful!")
}

func main() {
    http.HandleFunc("/", rateLimitedHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Burst Mode

The rate.NewLimiter function takes two arguments:

  • Rate: 5 requests per second.
  • Burst: Allows up to 5 requests to burst in a shorter time window.

Custom Middleware for Rate Limiting

You can create custom middleware to apply rate limiting to specific routes in your HTTP server.

Example

package main

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

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

type rateLimiter struct {
    limiter *rate.Limiter
}

func newRateLimiter(r rate.Limit, b int) *rateLimiter {
    return &rateLimiter{limiter: rate.NewLimiter(r, b)}
}

func (rl *rateLimiter) limit(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !rl.limiter.Allow() {
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func main() {
    limiter := newRateLimiter(2, 5) // 2 requests per second with a burst of 5
    handler := limiter.limit(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Request successful!")
    }))

    http.Handle("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Using Fixed Windows for Rate Limiting

Fixed window rate limiting can be implemented using a map and a time window.

Example

package main

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

var (
    rateLimitWindow = 1 * time.Minute
    rateLimit       = 10
    clients         = make(map[string]int)
    mu              sync.Mutex
)

func rateLimitedHandler(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    defer mu.Unlock()

    clientIP := r.RemoteAddr
    // Reset the count based on time window
    if count, found := clients[clientIP]; found {
        if count >= rateLimit {
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }
        clients[clientIP]++
    } else {
        clients[clientIP] = 1
        go func() {
            time.Sleep(rateLimitWindow)
            mu.Lock()
            delete(clients, clientIP)
            mu.Unlock()
        }()
    }

    // Simulate work
    fmt.Fprintln(w, "Request successful!")
}

func main() {
    http.HandleFunc("/", rateLimitedHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Using Redis for Distributed Rate Limiting

Using Redis can help in implementing rate limiting across multiple distributed instances of an application.

Example

package main

import (
    "fmt"
    "github.com/go-redis/redis/v8"
    "golang.org/x/net/context"
    "net/http"
    "time"
)

var (
    rdb = redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    ctx = context.Background()
)

func rateLimitedHandler(w http.ResponseWriter, r *http.Request) {
    clientIP := r.RemoteAddr
    key := "rate_limit:" + clientIP

    // Increment the rate limit counter in Redis
    count, err := rdb.Incr(ctx, key).Result()
    if err != nil {
        http.Error(w, fmt.Sprintf("Redis error: %v", err), http.StatusInternalServerError)
        return
    }

    // Set expiration on the key for the fixed window
    if count == 1 {
        rdb.Expire(ctx, key, time.Minute)
    }

    // Limit to 10 requests per minute
    if count > 10 {
        http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
        return
    }

    fmt.Fprintln(w, "Request successful!")
}

func main() {
    http.HandleFunc("/", rateLimitedHandler)
    http.ListenAndServe(":8080", nil)
}

By using these approaches, you can implement effective rate limiting in your Go applications, whether you are dealing with simple scenarios or complex, distributed environments.