Backend15 min read

API Rate Limiting Patterns in Go

Implementing token bucket, sliding window, and distributed rate limiting in Go with Redis backing.

GoAPIRate LimitingRedis

Why Rate Limiting?

Rate limiting protects your API from abuse, prevents resource exhaustion, and ensures fair usage among clients. Without it, a single misbehaving client can take down your entire service.

Token Bucket Algorithm

The token bucket is the most common rate limiting algorithm. Tokens are added to a bucket at a fixed rate, and each request consumes a token. When the bucket is empty, requests are rejected.

go
package ratelimit

import (
    "sync"
    "time"
)

type TokenBucket struct {
    capacity   int
    tokens     int
    refillRate time.Duration
    mu         sync.Mutex
    lastRefill time.Time
}

func NewTokenBucket(capacity int, refillRate time.Duration) *TokenBucket {
    return &TokenBucket{
        capacity:   capacity,
        tokens:     capacity,
        refillRate: refillRate,
        lastRefill: time.Now(),
    }
}

func (tb *TokenBucket) Allow() bool {
    tb.mu.Lock()
    defer tb.mu.Unlock()

    // Refill tokens based on elapsed time
    now := time.Now()
    elapsed := now.Sub(tb.lastRefill)
    tokensToAdd := int(elapsed / tb.refillRate)

    if tokensToAdd > 0 {
        tb.tokens = min(tb.capacity, tb.tokens+tokensToAdd)
        tb.lastRefill = now
    }

    // Check if request is allowed
    if tb.tokens > 0 {
        tb.tokens--
        return true
    }
    return false
}

Distributed Rate Limiting with Redis

For distributed systems, you need centralized rate limiting. Redis is perfect for this - it's fast and supports atomic operations:

go
func (r *RedisRateLimiter) Allow(ctx context.Context, key string) (bool, error) {
    script := redis.NewScript(`
        local tokens = redis.call('GET', KEYS[1])
        if tokens == false then
            tokens = ARGV[1]
        end
        tokens = tonumber(tokens)
        if tokens > 0 then
            redis.call('DECR', KEYS[1])
            redis.call('EXPIRE', KEYS[1], ARGV[2])
            return 1
        end
        return 0
    `)

    result, err := script.Run(ctx, r.client, []string{key}, r.capacity, r.window).Int()
    if err != nil {
        return false, err
    }
    return result == 1, nil
}

Middleware Implementation

Rate limiting is typically implemented as HTTP middleware. Here's how to integrate it with a standard Go HTTP server:

go
func RateLimitMiddleware(limiter *RedisRateLimiter) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Use client IP as rate limit key
            key := "ratelimit:" + getClientIP(r)

            allowed, err := limiter.Allow(r.Context(), key)
            if err != nil {
                http.Error(w, "Internal Server Error", 500)
                return
            }

            if !allowed {
                w.Header().Set("Retry-After", "60")
                http.Error(w, "Rate limit exceeded", 429)
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

Found this helpful?

I write about infrastructure, backend development, and DevOps. Follow along as I continue building.