Master Go Rate Limiting: Sliding Window, Token Bucket, and Redis Techniques

This article presents four practical Go rate‑limiting implementations—a sliding‑window algorithm, a token‑bucket approach, the built‑in golang.org/x/time/rate package, and a Redis‑backed distributed limiter—complete with code samples, usage guidance, and recommendations for different deployment scenarios.

Go Development Architecture Practice
Go Development Architecture Practice
Go Development Architecture Practice
Master Go Rate Limiting: Sliding Window, Token Bucket, and Redis Techniques

Introduction

Rate limiting is essential for protecting services from overload and abuse. In Go, several common strategies can be used, each with its own trade‑offs. The following sections describe four practical implementations and show how to choose the right one for your environment.

Scheme 1: Sliding Window (Recommended)

This method keeps a timestamp list for each user and removes entries that fall outside the configured time window. It provides precise request counting within the window.

package main

import (
    "sync"
    "time"
)

type RateLimiter struct {
    mu       sync.Mutex
    requests map[string][]time.Time
    limit    int
    window   time.Duration
}

func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
    return &RateLimiter{
        requests: make(map[string][]time.Time),
        limit:    limit,
        window:   window,
    }
}

func (rl *RateLimiter) Allow(userID string) bool {
    rl.mu.Lock()
    defer rl.mu.Unlock()

    now := time.Now()
    windowStart := now.Add(-rl.window)

    // Clean up expired request records
    validRequests := make([]time.Time, 0)
    for _, t := range rl.requests[userID] {
        if t.After(windowStart) {
            validRequests = append(validRequests, t)
        }
    }

    // Check if the limit is exceeded
    if len(validRequests) >= rl.limit {
        return false
    }

    // Add the new request
    validRequests = append(validRequests, now)
    rl.requests[userID] = validRequests

    // Optional periodic cleanup to avoid memory leaks
    if len(validRequests) == 1 {
        go rl.cleanup(userID, now)
    }
    return true
}

func (rl *RateLimiter) cleanup(userID string, baseTime time.Time) {
    time.Sleep(rl.window)
    rl.mu.Lock()
    defer rl.mu.Unlock()
    if requests, exists := rl.requests[userID]; exists {
        windowStart := baseTime.Add(-rl.window)
        validRequests := make([]time.Time, 0)
        for _, t := range requests {
            if t.After(windowStart) {
                validRequests = append(validRequests, t)
            }
        }
        if len(validRequests) == 0 {
            delete(rl.requests, userID)
        } else {
            rl.requests[userID] = validRequests
        }
    }
}

Scheme 2: Token Bucket Algorithm

The token bucket allows bursts up to a configured capacity while maintaining a steady refill rate, making it suitable for scenarios that require smooth traffic shaping.

package main

import (
    "sync"
    "time"
)

type TokenBucket struct {
    mu         sync.Mutex
    tokens     float64
    maxTokens  float64
    refillRate float64 // tokens per second
    lastRefill time.Time
}

func NewTokenBucket(maxTokens, refillRate float64) *TokenBucket {
    return &TokenBucket{tokens: maxTokens, maxTokens: maxTokens, refillRate: refillRate, lastRefill: time.Now()}
}

func (tb *TokenBucket) refill() {
    now := time.Now()
    duration := now.Sub(tb.lastRefill)
    tokensToAdd := tb.refillRate * duration.Seconds()
    tb.tokens += tokensToAdd
    if tb.tokens > tb.maxTokens {
        tb.tokens = tb.maxTokens
    }
    tb.lastRefill = now
}

func (tb *TokenBucket) Take(tokens float64) bool {
    tb.mu.Lock()
    defer tb.mu.Unlock()
    tb.refill()
    if tb.tokens >= tokens {
        tb.tokens -= tokens
        return true
    }
    return false
}

type UserRateLimiter struct {
    mu      sync.RWMutex
    users   map[string]*TokenBucket
    limit   int
    window  time.Duration
}

func NewUserRateLimiter(limit int, window time.Duration) *UserRateLimiter {
    refillRate := float64(limit) / window.Seconds()
    return &UserRateLimiter{users: make(map[string]*TokenBucket), limit: limit, window: window}
}

func (rl *UserRateLimiter) Allow(userID string) bool {
    rl.mu.RLock()
    bucket, exists := rl.users[userID]
    rl.mu.RUnlock()
    if !exists {
        rl.mu.Lock()
        bucket = NewTokenBucket(float64(rl.limit), float64(rl.limit)/rl.window.Seconds())
        rl.users[userID] = bucket
        rl.mu.Unlock()
    }
    return bucket.Take(1)
}

Scheme 3: Using golang.org/x/time/rate Package

The official Go rate package provides a ready‑made token‑bucket implementation with thread‑safe primitives.

package main

import (
    "sync"
    "time"
    "golang.org/x/time/rate"
)

type RateLimiter struct {
    mu      sync.RWMutex
    users   map[string]*rate.Limiter
    limit   rate.Limit
    burst   int
}

func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
    r := rate.Every(window / time.Duration(limit))
    return &RateLimiter{users: make(map[string]*rate.Limiter), limit: r, burst: limit}
}

func (rl *RateLimiter) GetLimiter(userID string) *rate.Limiter {
    rl.mu.RLock()
    limiter, exists := rl.users[userID]
    rl.mu.RUnlock()
    if !exists {
        rl.mu.Lock()
        limiter = rate.NewLimiter(rl.limit, rl.burst)
        rl.users[userID] = limiter
        rl.mu.Unlock()
    }
    return limiter
}

func (rl *RateLimiter) Allow(userID string) bool {
    return rl.GetLimiter(userID).Allow()
}

func (rl *RateLimiter) CleanupInactiveUsers(timeout time.Duration) {
    ticker := time.NewTicker(time.Hour)
    go func() {
        for range ticker.C {
            rl.mu.Lock()
            for userID, limiter := range rl.users {
                // Placeholder for real inactivity check
                _ = limiter
                delete(rl.users, userID)
            }
            rl.mu.Unlock()
        }
    }()
}

Scheme 4: Redis‑Based Distributed Limiter

For multi‑instance deployments, a Redis script can enforce limits atomically across processes.

package main

import (
    "context"
    "fmt"
    "time"
    "github.com/go-redis/redis/v8"
)

type RedisRateLimiter struct {
    client *redis.Client
    limit  int
    window time.Duration
}

func NewRedisRateLimiter(client *redis.Client, limit int, window time.Duration) *RedisRateLimiter {
    return &RedisRateLimiter{client: client, limit: limit, window: window}
}

func (rl *RedisRateLimiter) Allow(ctx context.Context, userID string) (bool, error) {
    now := time.Now().UnixNano()
    windowSize := rl.window.Nanoseconds()
    key := fmt.Sprintf("rate_limit:%s", userID)
    luaScript := `
        local key = KEYS[1]
        local now = tonumber(ARGV[1])
        local window = tonumber(ARGV[2])
        local limit = tonumber(ARGV[3])
        redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
        local current = redis.call('ZCARD', key)
        if current < limit then
            redis.call('ZADD', key, now, now)
            redis.call('EXPIRE', key, math.ceil(window / 1000000000))
            return 1
        end
        return 0
    `
    result, err := rl.client.Eval(ctx, luaScript, []string{key}, now, windowSize, rl.limit).Result()
    if err != nil {
        return false, err
    }
    return result.(int64) == 1, nil
}

Usage Example

The following snippet shows how to plug a limiter into an HTTP handler, using the sliding‑window implementation as an example.

package main

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

func main() {
    limiter := NewRateLimiter(1000, time.Minute)
    http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
        userID := r.Header.Get("X-User-ID")
        if userID == "" {
            userID = r.RemoteAddr // fallback identifier
        }
        if !limiter.Allow(userID) {
            http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
            return
        }
        fmt.Fprint(w, "Request successful")
    })
    http.ListenAndServe(":8080", nil)
}

Recommendations

Single‑node applications : Use Scheme 1 (sliding window) or Scheme 3 (golang.org/x/time/rate) for simplicity and efficiency.

Distributed systems : Adopt Scheme 4 with Redis to keep limits consistent across instances.

Need smooth throttling with bursts : Choose Scheme 2 (token bucket) to allow occasional spikes.

Production environments : Combine the limiter with monitoring and alerting to track rejected requests.

Choosing the right approach depends on factors such as distribution requirements, memory constraints, and precision needs.

Source: Go language community article (originally published on WeChat).

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Redisrate limitingToken Bucketgolang.org/x/time/ratesliding-window
Go Development Architecture Practice
Written by

Go Development Architecture Practice

Daily sharing of Golang-related technical articles, practical resources, language news, tutorials, real-world projects, and more. Looking forward to growing together. Let's go!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.