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.
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 protection8. 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
