Mastering Redis Rate Limiting: Token Bucket & Sliding Window Full Implementation Guide

This article provides a comprehensive, production‑ready walkthrough of implementing Redis‑based rate limiting using token bucket and sliding window algorithms, covering algorithm fundamentals, code examples, performance testing, multi‑layer architecture, dynamic configuration, and best‑practice recommendations for high‑traffic backend services.

Cloud Architecture
Cloud Architecture
Cloud Architecture
Mastering Redis Rate Limiting: Token Bucket & Sliding Window Full Implementation Guide

Preface

Rate limiting is the final defense line for protecting high‑concurrency systems such as API gateways, microservice interfaces, and distributed task schedulers.

1. Rate‑Limiting Algorithm Overview

1.1 Comparison Matrix

┌─────────────────────────────────────────────────────────────────────────┐
│                     Rate‑Limiting Algorithm Matrix                     │
├──────────────┬─────────────┬─────────────┬─────────────┬───────────────┤
│ Algorithm    │ Difficulty  │ Memory      │ Precision   │ Suitable Scenarios │
├──────────────┼─────────────┼─────────────┼─────────────┼───────────────┤
│ Fixed Window │ ⭐           │ O(1)        │ Low         │ Simple scenarios │
│ Sliding Window│ ⭐⭐          │ O(N)        │ High        │ Precise limiting │
│ Token Bucket │ ⭐⭐          │ O(1)        │ Medium      │ Allows bursts   │
│ Leaky Bucket │ ⭐⭐          │ O(1)        │ Medium      │ Smooth outflow   │
│ Distributed  │ ⭐⭐⭐         │ O(N)        │ High        │ Cluster environments │
└──────────────┴─────────────┴─────────────┴─────────────┴───────────────┘

1.2 Visualized Principles

┌─────────────────────────────────────────────────────────────────────────┐
│               Fixed Window Counter (example)                         │
└─────────────────────────────────────────────────────────────────────────┘
Time axis: 0s   60s   120s   180s
│──────────│──────────│──────────│
Window 1  ██████████│          │ 100 requests
          │          ██████████│ 100 requests (Window 2)
          │          │          ██████████│ 100 requests (Window 3)
Problem: burst at window boundary (0:59 → 100, 1:01 → 100 = 200 in 2 s)

┌─────────────────────────────────────────────────────────────────────────┐
│               Sliding Window (60 s)                                 │
└─────────────────────────────────────────────────────────────────────────┘
Each cell = 15 s, count recent 4 cells → smooth transition, no boundary issue.

┌─────────────────────────────────────────────────────────────────────────┐
│               Token Bucket (capacity 100, rate 10/s)                 │
└─────────────────────────────────────────────────────────────────────────┘
Tokens generated at fixed rate; requests consume tokens; allows bursts while tokens remain.

2. Why Redis Suits Rate Limiting

2.1 Core Advantages

┌─────────────────────────────────────────────────────────────────────────┐
│               Redis Rate‑Limiting Core Advantages                     │
├─────────────────────────────────────────────────────────────────────────┤
│ 🚀 High performance – single‑node QPS up to 100 k+, µs latency      │
│ 🔢 Atomic operations – INCR/DECR, Lua scripts guarantee atomicity   │
│ ⏰ Expiration – native TTL, automatic cleanup                     │
│ 📦 Rich data structures – String (counter), ZSet (sliding window), │
│      List (queue), Hash (multidimensional)                         │
└─────────────────────────────────────────────────────────────────────────┘

2.2 Applicable Scenarios

API gateway: Token bucket + Redis – supports bursts, precise control.

User interface: Sliding window + Redis – prevents scraping.

SMS/Email: Fixed window + Redis – simple and effective.

Distributed task scheduling: Redis + Lua – ensures atomicity.

Microservice circuit breaking: Token bucket + local cache – multi‑layer protection.

3. Fixed Window Counter Implementation

3.1 Basic Implementation

// ratelimit/fixed_window.go
package ratelimit

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

type FixedWindowLimiter struct {
    client *redis.Client
}

func NewFixedWindowLimiter(client *redis.Client) *FixedWindowLimiter {
    return &FixedWindowLimiter{client: client}
}

// Allow checks whether a request is permitted.
func (l *FixedWindowLimiter) Allow(ctx context.Context, key string, limit int64, window int64) (bool, error) {
    redisKey := fmt.Sprintf("rate:fixed:%s", key)
    count, err := l.client.Incr(ctx, redisKey).Result()
    if err != nil {
        return false, err
    }
    if count == 1 {
        if err = l.client.Expire(ctx, redisKey, time.Duration(window)*time.Second).Err(); err != nil {
            return false, err
        }
    }
    return count <= limit, nil
}

type LimitInfo struct {
    Allowed   bool   `json:"allowed"`
    Count     int64  `json:"count"`
    Limit     int64  `json:"limit"`
    Remaining int64  `json:"remaining"`
    ResetAt   time.Time `json:"reset_at"`
}

// AllowWithInfo returns detailed limit information.
func (l *FixedWindowLimiter) AllowWithInfo(ctx context.Context, key string, limit int64, window int64) (*LimitInfo, error) {
    redisKey := fmt.Sprintf("rate:fixed:%s", key)
    pipe := l.client.Pipeline()
    incrCmd := pipe.Incr(ctx, redisKey)
    ttlCmd := pipe.TTL(ctx, redisKey)
    if _, err := pipe.Exec(ctx); err != nil {
        return nil, err
    }
    count := incrCmd.Val()
    ttl := ttlCmd.Val()
    if count == 1 {
        l.client.Expire(ctx, redisKey, time.Duration(window)*time.Second)
        ttl = time.Duration(window) * time.Second
    }
    remaining := limit - count
    if remaining < 0 {
        remaining = 0
    }
    return &LimitInfo{Allowed: count <= limit, Count: count, Limit: limit, Remaining: remaining, ResetAt: time.Now().Add(ttl)}, nil
}

3.3 Fixed Window Boundary Issue Demo

// Demonstrate window boundary problem
func demonstrateWindowBoundaryIssue() {
    // Window size = 60 s, limit = 100
    // 0:59 → 100 requests (Window 1)
    // 1:01 → 100 requests (Window 2)
    // 200 requests processed in 2 s! This is the "critical problem" of fixed windows.
    // Solution: use sliding window.
}

4. Sliding Window Implementation

4.1 Sliding Window Based on Redis ZSet

// ratelimit/sliding_window.go
package ratelimit

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

type SlidingWindowLimiter struct {
    client *redis.Client
}

func NewSlidingWindowLimiter(client *redis.Client) *SlidingWindowLimiter {
    return &SlidingWindowLimiter{client: client}
}

// Allow checks request permission using a Lua script for atomicity.
func (l *SlidingWindowLimiter) Allow(ctx context.Context, key string, limit int64, window int64) (bool, error) {
    redisKey := fmt.Sprintf("rate:sliding:%s", key)
    now := time.Now().UnixNano() / 1e6 // current time in ms
    script := redis.NewScript(`
        local key = KEYS[1]
        local limit = tonumber(ARGV[1])
        local window = tonumber(ARGV[2])
        local now = tonumber(ARGV[3])
        local timeout = tonumber(ARGV[4])
        local minScore = now - window
        redis.call('ZREMRANGEBYSCORE', key, '-inf', minScore)
        local count = redis.call('ZCARD', key)
        if count >= limit then
            return 0
        end
        redis.call('ZADD', key, now, now .. '-' .. math.random(1000000))
        redis.call('PEXPIRE', key, timeout)
        return 1
    `)
    result, err := script.Run(ctx, l.client, []string{redisKey}, limit, window, now, window*2).Int()
    if err != nil {
        return false, err
    }
    return result == 1, nil
}

// AllowWithInfo returns detailed sliding‑window data.
func (l *SlidingWindowLimiter) AllowWithInfo(ctx context.Context, key string, limit int64, window int64) (*LimitInfo, error) {
    redisKey := fmt.Sprintf("rate:sliding:%s", key)
    now := time.Now().UnixNano() / 1e6
    script := redis.NewScript(`
        local key = KEYS[1]
        local limit = tonumber(ARGV[1])
        local window = tonumber(ARGV[2])
        local now = tonumber(ARGV[3])
        local timeout = tonumber(ARGV[4])
        local minScore = now - window
        redis.call('ZREMRANGEBYSCORE', key, '-inf', minScore)
        local count = redis.call('ZCARD', key)
        local remaining = limit - count
        local allowed = 0
        if count < limit then
            allowed = 1
            redis.call('ZADD', key, now, now .. '-' .. math.random(1000000))
            remaining = remaining - 1
        end
        redis.call('PEXPIRE', key, timeout)
        local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
        local resetAt = now + window
        if #oldest >= 2 then
            resetAt = tonumber(oldest[2]) + window
        end
        return {allowed, count, remaining, resetAt}
    `)
    result, err := script.Run(ctx, l.client, []string{redisKey}, limit, window, now, window*2).Result()
    if err != nil {
        return nil, err
    }
    vals := result.([]interface{})
    allowed := vals[0].(int64) == 1
    count := vals[1].(int64)
    remaining := vals[2].(int64)
    resetAt := vals[3].(int64)
    return &LimitInfo{Allowed: allowed, Count: count, Limit: limit, Remaining: remaining, ResetAt: time.Unix(resetAt/1000, (resetAt%1000)*1e6)}, nil
}

type WindowStats struct {
    TotalRequests   int   `json:"total_requests"`
    WindowSize      int64 `json:"window_size"`
    QPSDistribution map[int64]int `json:"qps_distribution"`
}

func (l *SlidingWindowLimiter) GetWindowStats(ctx context.Context, key string, window int64) (*WindowStats, error) {
    redisKey := fmt.Sprintf("rate:sliding:%s", key)
    now := time.Now().UnixNano() / 1e6
    minScore := now - window
    results, err := l.client.ZRangeByScore(ctx, redisKey, &redis.ZRangeBy{Min: fmt.Sprintf("%d", minScore), Max: fmt.Sprintf("%d", now)}).Result()
    if err != nil {
        return nil, err
    }
    qpsMap := make(map[int64]int)
    for _, r := range results {
        ts, _ := time.Parse(time.RFC3339, r)
        sec := ts.Unix()
        qpsMap[sec]++
    }
    return &WindowStats{TotalRequests: len(results), WindowSize: window, QPSDistribution: qpsMap}, nil
}

4.2 Fixed vs Sliding Window Test

// ratelimit/sliding_window_test.go
package ratelimit

import (
    "context"
    "testing"
    "time"
)
func TestFixedWindowVsSlidingWindow(t *testing.T) {
    ctx := context.Background()
    client := getRedisClient()
    fixedLimiter := NewFixedWindowLimiter(client)
    slidingLimiter := NewSlidingWindowLimiter(client)

    t.Run("FixedWindow-BoundaryIssue", func(t *testing.T) {
        for i := 0; i < 100; i++ {
            allowed, _ := fixedLimiter.Allow(ctx, "user:fixed", 100, 60)
            if !allowed {
                t.Errorf("request at 59 s rejected")
            }
        }
        time.Sleep(2 * time.Second)
        for i := 0; i < 100; i++ {
            allowed, _ := fixedLimiter.Allow(ctx, "user:fixed", 100, 60)
            if !allowed {
                t.Errorf("request at 61 s rejected")
            }
        }
        // 200 requests processed in 3 s!
    })

    t.Run("SlidingWindow-SmoothLimit", func(t *testing.T) {
        for i := 0; i < 100; i++ {
            allowed, _ := slidingLimiter.Allow(ctx, "user:sliding", 100, 60000)
            if !allowed {
                t.Errorf("request at 59 s rejected")
            }
        }
        time.Sleep(2 * time.Second)
        allowed, _ := slidingLimiter.Allow(ctx, "user:sliding", 100, 60000)
        if allowed {
            t.Errorf("sliding window should reject request at 61 s")
        }
    })
}

5. Token Bucket Implementation

5.1 Basic Token Bucket

// ratelimit/token_bucket.go
package ratelimit

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

type TokenBucketLimiter struct {
    client *redis.Client
}

func NewTokenBucketLimiter(client *redis.Client) *TokenBucketLimiter {
    return &TokenBucketLimiter{client: client}
}

// Allow tries to take one token.
func (l *TokenBucketLimiter) Allow(ctx context.Context, key string, capacity int64, refillRate float64) (bool, error) {
    redisKey := fmt.Sprintf("rate:bucket:%s", key)
    now := time.Now().UnixNano() / 1e9 // seconds as float
    script := redis.NewScript(`
        local key = KEYS[1]
        local capacity = tonumber(ARGV[1])
        local refillRate = tonumber(ARGV[2])
        local now = tonumber(ARGV[3])
        local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
        local tokens = tonumber(bucket[1])
        local lastRefill = tonumber(bucket[2])
        if tokens == nil then
            tokens = capacity
            lastRefill = now
        end
        local elapsed = now - lastRefill
        local newTokens = elapsed * refillRate
        tokens = math.min(capacity, tokens + newTokens)
        local allowed = 0
        if tokens >= 1 then
            tokens = tokens - 1
            allowed = 1
        end
        redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
        redis.call('EXPIRE', key, math.ceil(capacity / refillRate) + 60)
        return {allowed, math.floor(tokens)}
    `)
    result, err := script.Run(ctx, l.client, []string{redisKey}, capacity, refillRate, now).Result()
    if err != nil {
        return false, err
    }
    allowed := result.([]interface{})[0].(int64) == 1
    return allowed, nil
}

// AllowN tries to take N tokens.
func (l *TokenBucketLimiter) AllowN(ctx context.Context, key string, capacity int64, refillRate float64, n int64) (bool, error) {
    redisKey := fmt.Sprintf("rate:bucket:%s", key)
    now := time.Now().UnixNano() / 1e9
    script := redis.NewScript(`
        local key = KEYS[1]
        local capacity = tonumber(ARGV[1])
        local refillRate = tonumber(ARGV[2])
        local now = tonumber(ARGV[3])
        local n = tonumber(ARGV[4])
        local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
        local tokens = tonumber(bucket[1])
        local lastRefill = tonumber(bucket[2])
        if tokens == nil then
            tokens = capacity
            lastRefill = now
        end
        local elapsed = now - lastRefill
        local newTokens = elapsed * refillRate
        tokens = math.min(capacity, tokens + newTokens)
        local allowed = 0
        if tokens >= n then
            tokens = tokens - n
            allowed = 1
        end
        redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
        redis.call('EXPIRE', key, math.ceil(capacity / refillRate) + 60)
        return {allowed, math.floor(tokens)}
    `)
    result, err := script.Run(ctx, l.client, []string{redisKey}, capacity, refillRate, now, n).Result()
    if err != nil {
        return false, err
    }
    allowed := result.([]interface{})[0].(int64) == 1
    return allowed, nil
}

type TokenBucketInfo struct {
    Allowed          bool    `json:"allowed"`
    Remaining        int64   `json:"remaining"`
    Capacity         int64   `json:"capacity"`
    RefillRate       float64 `json:"refill_rate"`
    WaitTimeSeconds  float64 `json:"wait_time_seconds"`
}

func (l *TokenBucketLimiter) AllowWithInfo(ctx context.Context, key string, capacity int64, refillRate float64) (*TokenBucketInfo, error) {
    redisKey := fmt.Sprintf("rate:bucket:%s", key)
    now := time.Now().UnixNano() / 1e9
    script := redis.NewScript(`
        local key = KEYS[1]
        local capacity = tonumber(ARGV[1])
        local refillRate = tonumber(ARGV[2])
        local now = tonumber(ARGV[3])
        local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
        local tokens = tonumber(bucket[1])
        local lastRefill = tonumber(bucket[2])
        if tokens == nil then
            tokens = capacity
            lastRefill = now
        end
        local elapsed = now - lastRefill
        local newTokens = elapsed * refillRate
        tokens = math.min(capacity, tokens + newTokens)
        local allowed = 0
        if tokens >= 1 then
            tokens = tokens - 1
            allowed = 1
        end
        redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
        redis.call('EXPIRE', key, math.ceil(capacity / refillRate) + 60)
        local waitTime = 0
        if allowed == 0 then
            waitTime = (1 - tokens) / refillRate
        end
        return {allowed, math.floor(tokens), waitTime}
    `)
    result, err := script.Run(ctx, l.client, []string{redisKey}, capacity, refillRate, now).Result()
    if err != nil {
        return nil, err
    }
    vals := result.([]interface{})
    allowed := vals[0].(int64) == 1
    remaining := vals[1].(int64)
    waitTime := vals[2].(float64)
    return &TokenBucketInfo{Allowed: allowed, Remaining: remaining, Capacity: capacity, RefillRate: refillRate, WaitTimeSeconds: waitTime}, nil
}

5.2 Token Bucket vs Sliding Window Scenarios

// Demonstrate Token Bucket vs Sliding Window
func demonstrateTokenBucketVsSlidingWindow() {
    // API limit: 10 req/s, allow bursts.
    // Token Bucket (capacity 10, rate 10/s)
    //   t=0 s: bucket full (10 tokens)
    //   10 immediate requests → ✅ all pass, bucket empty
    //   t=0.1 s: next request → ❌ rejected, need 0.1 s to refill
    //   t=0.5 s: request → ✅ passes (5 tokens replenished)
    //   Feature: bursts allowed, then cooldown.
    // Sliding Window (1 s window, limit 10)
    //   t=0 s: 10 requests → ✅
    //   t=0.5 s: new request → ❌ (window already full)
    //   t=1 s: request → ✅ (oldest requests fall out)
    //   Feature: strict per‑second limit, no bursts.
}

6. Production‑Grade Rate‑Limiting Component Design

6.1 Multi‑Layer Architecture

// ratelimit/multi_layer.go
package ratelimit

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

type MultiLayerLimiter struct {
    localLimiter  *LocalLimiter
    redisLimiter  *TokenBucketLimiter
    fallbackPolicy FallbackPolicy
}

type FallbackPolicy int

const (
    FallbackAllow FallbackPolicy = iota // allow on Redis error
    FallbackDeny                        // deny on Redis error
    FallbackLocal                       // fall back to local limiter result
)

func NewMultiLayerLimiter(client *redis.Client, localRate int64, localBurst int64, fallback FallbackPolicy) *MultiLayerLimiter {
    return &MultiLayerLimiter{localLimiter: NewLocalLimiter(localRate, localBurst), redisLimiter: NewTokenBucketLimiter(client), fallbackPolicy: fallback}
}

func (l *MultiLayerLimiter) Allow(ctx context.Context, key string, capacity int64, refillRate float64) (bool, error) {
    if !l.localLimiter.Allow() {
        return false, errors.New("local rate limit exceeded")
    }
    allowed, err := l.redisLimiter.Allow(ctx, key, capacity, refillRate)
    if err != nil {
        switch l.fallbackPolicy {
        case FallbackAllow:
            return true, nil
        case FallbackDeny:
            return false, err
        case FallbackLocal:
            return true, nil
        }
    }
    return allowed, err
}

// LocalLimiter (in‑process token bucket)
type LocalLimiter struct {
    rate        int64
    burst       int64
    tokens      int64
    lastRefill time.Time
    mu         sync.Mutex
}

func NewLocalLimiter(rate int64, burst int64) *LocalLimiter {
    return &LocalLimiter{rate: rate, burst: burst, tokens: burst, lastRefill: time.Now()}
}

func (l *LocalLimiter) Allow() bool {
    l.mu.Lock()
    defer l.mu.Unlock()
    now := time.Now()
    elapsed := now.Sub(l.lastRefill).Seconds()
    newTokens := int64(elapsed * float64(l.rate))
    if l.tokens+newTokens > l.burst {
        l.tokens = l.burst
    } else {
        l.tokens += newTokens
    }
    l.lastRefill = now
    if l.tokens > 0 {
        l.tokens--
        return true
    }
    return false
}

6.2 Rate‑Limiting Middleware (Gin example)

// ratelimit/middleware.go
package ratelimit

import (
    "context"
    "fmt"
    "net/http"
    "strconv"
    "time"
    "github.com/go-redis/redis/v8"
    "github.com/gin-gonic/gin"
)

type RateLimitMiddleware struct {
    limiter *SlidingWindowLimiter
    config  MiddlewareConfig
}

type MiddlewareConfig struct {
    Limit        int64         `json:"limit"`
    Window       time.Duration `json:"window"`
    KeyExtractor KeyExtractor  `json:"-"`
    Skipper      Skipper       `json:"-"`
}

type KeyExtractor func(c *gin.Context) string

type Skipper func(c *gin.Context) bool

func DefaultKeyExtractor(c *gin.Context) string {
    ip := c.GetHeader("X-Real-IP")
    if ip == "" {
        ip = c.GetHeader("X-Forwarded-For")
    }
    if ip == "" {
        ip = strings.Split(c.Request.RemoteAddr, ":")[0]
    }
    return fmt.Sprintf("ip:%s", ip)
}

func DefaultSkipper(c *gin.Context) bool { return false }

func NewRateLimitMiddleware(client *redis.Client, config MiddlewareConfig) *RateLimitMiddleware {
    if config.KeyExtractor == nil {
        config.KeyExtractor = DefaultKeyExtractor
    }
    if config.Skipper == nil {
        config.Skipper = DefaultSkipper
    }
    return &RateLimitMiddleware{limiter: NewSlidingWindowLimiter(client), config: config}
}

func (m *RateLimitMiddleware) Middleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        if m.config.Skipper(c) {
            c.Next()
            return
        }
        key := m.config.KeyExtractor(c)
        info, err := m.limiter.AllowWithInfo(c.Request.Context(), key, m.config.Limit, m.config.Window.Milliseconds())
        if err != nil {
            c.Next()
            return
        }
        c.Header("X-RateLimit-Limit", fmt.Sprintf("%d", info.Limit))
        c.Header("X-RateLimit-Remaining", fmt.Sprintf("%d", info.Remaining))
        c.Header("X-RateLimit-Reset", fmt.Sprintf("%d", info.ResetAt.Unix()))
        if !info.Allowed {
            c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"code": 429, "message": "Too Many Requests", "retry_after": int(time.Until(info.ResetAt).Seconds())})
            return
        }
        c.Next()
    }
}

6.3 Dynamic Rate‑Limiting Configuration

// ratelimit/dynamic.go
package ratelimit

import (
    "context"
    "encoding/json"
    "sync"
    "time"
    "github.com/go-redis/redis/v8"
)

type DynamicLimiter struct {
    client        *redis.Client
    configKey     string
    config        *RateLimitConfig
    configMu      sync.RWMutex
    reloadTicker  *time.Ticker
    stopChan      chan struct{}
}

type RateLimitConfig struct {
    Enabled    bool    `json:"enabled"`
    Algorithm  string  `json:"algorithm"` // fixed, sliding, token_bucket
    Limit      int64   `json:"limit"`
    Window     int64   `json:"window"` // seconds
    Capacity   int64   `json:"capacity"`
    RefillRate float64 `json:"refill_rate"`
    Whitelist  []string `json:"whitelist"`
    Blacklist  []string `json:"blacklist"`
}

func NewDynamicLimiter(client *redis.Client, configKey string, reloadInterval time.Duration) *DynamicLimiter {
    dl := &DynamicLimiter{client: client, configKey: configKey, reloadTicker: time.NewTicker(reloadInterval), stopChan: make(chan struct{})}
    dl.loadConfig(context.Background())
    go dl.watchConfig()
    return dl
}

func (l *DynamicLimiter) loadConfig(ctx context.Context) error {
    data, err := l.client.Get(ctx, l.configKey).Bytes()
    if err != nil {
        return err
    }
    var cfg RateLimitConfig
    if err = json.Unmarshal(data, &cfg); err != nil {
        return err
    }
    l.configMu.Lock()
    l.config = &cfg
    l.configMu.Unlock()
    return nil
}

func (l *DynamicLimiter) watchConfig() {
    for {
        select {
        case <-l.reloadTicker.C:
            l.loadConfig(context.Background())
        case <-l.stopChan:
            l.reloadTicker.Stop()
            return
        }
    }
}

func (l *DynamicLimiter) Allow(ctx context.Context, key string) (bool, error) {
    l.configMu.RLock()
    cfg := l.config
    l.configMu.RUnlock()
    if cfg == nil || !cfg.Enabled {
        return true, nil
    }
    for _, p := range cfg.Whitelist {
        if strings.HasPrefix(key, p) {
            return true, nil
        }
    }
    for _, p := range cfg.Blacklist {
        if strings.HasPrefix(key, p) {
            return false, nil
        }
    }
    switch cfg.Algorithm {
    case "fixed":
        return NewFixedWindowLimiter(l.client).Allow(ctx, key, cfg.Limit, cfg.Window)
    case "sliding":
        return NewSlidingWindowLimiter(l.client).Allow(ctx, key, cfg.Limit, cfg.Window*1000)
    case "token_bucket":
        return NewTokenBucketLimiter(l.client).Allow(ctx, key, cfg.Capacity, cfg.RefillRate)
    default:
        return true, nil
    }
}

func (l *DynamicLimiter) Stop() { close(l.stopChan) }

6.4 Real‑World Use Cases

SMS verification: per‑minute, per‑hour, and daily limits with cooldown keys.

API gateway: global system‑wide limit, per‑API limit, and per‑user token‑bucket limit.

Distributed task execution: concurrency slot acquisition via Lua script and frequency limiting via sliding window.

7. Performance Optimisation & Best Practices

7.1 Lua Script Optimisation

-- Prefer batch operations inside a single script to minimise round‑trips.
-- Example: combine HMGET, token calculation, HMSET, and EXPIRE in one script.

7.2 Redis Connection‑Pool Tuning

func createOptimizedRedisClient() *redis.Client {
    return redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
        Password: "",
        DB: 0,
        PoolSize: 100,          // total connections
        MinIdleConns: 10,       // minimum idle
        MaxConnAge: time.Hour,
        PoolTimeout: 4 * time.Second,
        IdleTimeout: 5 * time.Minute,
        IdleCheckFrequency: time.Minute,
        DialTimeout: 5 * time.Second,
        ReadTimeout: 3 * time.Second,
        WriteTimeout: 3 * time.Second,
        MaxRetries: 3,
        MinRetryBackoff: 8 * time.Millisecond,
        MaxRetryBackoff: 512 * time.Millisecond,
    })
}

7.3 Monitoring Metrics (Prometheus)

// ratelimit/metrics.go
package ratelimit

import "github.com/prometheus/client_golang/prometheus"

var (
    rateLimitRequestsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{Name: "ratelimit_requests_total", Help: "Total number of rate‑limit checks"}, []string{"algorithm", "key", "result"})
    rateLimitDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{Name: "ratelimit_duration_seconds", Help: "Latency of rate‑limit processing", Buckets: prometheus.DefBuckets}, []string{"algorithm"})
    redisOperationDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{Name: "redis_operation_duration_seconds", Help: "Redis operation latency", Buckets: []float64{0.001,0.005,0.01,0.025,0.05,0.1,0.25,0.5,1}}, []string{"operation"})
    rateLimitCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "ratelimit_current", Help: "Current request count in window"}, []string{"key", "algorithm"})
)

func init() {
    prometheus.MustRegister(rateLimitRequestsTotal, rateLimitDuration, redisOperationDuration, rateLimitCurrent)
}

7.4 Configuration Recommendations

# Example configuration matrix (textual representation)
# Scenario                Algorithm      Config Example          Note
# ---------------------------------------------------------------
# API gateway global      Fixed Window   10000 req/s             Prevent system overload
# User API                Token Bucket   capacity=100, rate=10/s Allow bursts
# SMS verification        Sliding Window 1/min, 5/hour          Strict frequency control
# File upload             Token Bucket   capacity=10, 1/min      Limit upload rate
# Search interface        Sliding Window 30/min                  Guard against abuse
# Login attempts          Sliding Window 5/min                  Prevent brute‑force
# Comment posting         Token Bucket   capacity=20, 2/min      Allow short bursts
# Distributed tasks       Fixed + Concurrency 100/min, 10 concurrent  Dual protection

8. Summary

8.1 Algorithm Selection Guide

Need rate limiting?
   └─ Is precise control required?
        ├─ Yes → Sliding Window (exact)
        └─ No  → Fixed Window (simple)
   Then, do you need burst allowance?
        ├─ Yes → Token Bucket
        └─ No  → Sliding Window (strict)

8.2 Core Takeaways

Use Lua scripts for atomic composite operations.

Combine local in‑process limiter with Redis for multi‑layer protection.

Support dynamic configuration to adjust limits without redeploy.

Define graceful degradation strategies for Redis failures.

Instrument with Prometheus metrics for observability.

8.3 Best‑Practice Checklist

✅ Use Lua to minimise network round‑trips.

✅ Tune Redis connection pool and timeouts.

✅ Deploy multi‑layer rate limiting (local + distributed).

✅ Set up comprehensive monitoring and alerts.

✅ Plan Redis failure fallback policies.

✅ Conduct regular load testing to validate limits.

✅ Log rate‑limit events for post‑mortem analysis.

References

Redis official documentation.

Rate Limiting algorithm research papers.

Prometheus monitoring guide.

Go‑Redis client library.

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.

backendGolangredisRate LimitingLuasliding-windowtoken-bucket
Cloud Architecture
Written by

Cloud Architecture

Focuses on cloud‑native and distributed architecture engineering, sharing practical solutions and lessons learned. Covers microservice governance, Kubernetes, observability, and stability engineering to help your systems run stable, fast, and cost‑effectively.

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.