Rate Limiting with Redis: Token Bucket and Sliding Window in Go
Protecting APIs and edge endpoints almost always requires rate limiting. In-process counters work on a single machine; with multiple instances you need a shared store — typically Redis.
This article is a code-first golang rate limiting guide: how to implement token bucket and sliding window models on Redis with atomic semantics.
Why Redis?
| Scenario | In-memory | Redis |
|---|---|---|
| Single pod | Simple | Optional |
| Horizontal scale | Inconsistent | Single source of truth |
| Time windows | Awkward | ZSET / TTL fit naturally |
| Atomicity | Mutex | INCR, Lua, EVAL |
Token bucket
Idea: a bucket refills at a steady rate; each request spends tokens. No tokens → return 429 or Retry-After.
You can model counters with timestamps in Redis; a cleaner approach is a Lua script that updates state and returns allow/deny in one round trip.
Parameters
- Capacity: maximum tokens in the bucket
- Refill rate: tokens per second (or per minute)
Simplified Lua sketch
Refill based on elapsed time, subtract cost for the request, persist tokens and last timestamp:
-- KEYS[1] key, ARGV[1] capacity, ARGV[2] refill per second, ARGV[3] now ms, ARGV[4] cost
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local data = redis.call('HMGET', key, 'tokens', 'last')
local tokens = tonumber(data[1])
local last = tonumber(data[2])
if not tokens then
tokens = capacity
last = now
end
local delta = math.max(0, now - last)
local refill = delta * rate / 1000
tokens = math.min(capacity, tokens + refill)
if tokens >= cost then
tokens = tokens - cost
redis.call('HSET', key, 'tokens', tokens, 'last', now)
redis.call('PEXPIRE', key, 3600000)
return {1, tokens}
else
redis.call('HSET', key, 'tokens', tokens, 'last', now)
redis.call('PEXPIRE', key, 3600000)
return {0, tokens}
end
With github.com/redis/go-redis/v9:
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
now := float64(time.Now().UnixMilli())
res, err := rdb.Eval(ctx, script, []string{"ratelimit:user:42"},
100, 10.0, now, 1).Result()
For libraries, golang.org/x/time/rate is local-only; many teams wrap Redis + Lua or use community limiters. In production, add metrics for allowed vs rejected.
Sliding window
Fixed window counters can allow bursts at window edges. Sliding window counts events in the last N seconds.
Common Redis patterns:
- Sorted set: add request timestamp as score; remove old entries with
ZREMRANGEBYSCORE;ZCARDfor count. - Approximate algorithms for very high QPS.
ZSET flow
ZADD key now uniqueMemberZREMRANGEBYSCORE key 0 now-windowMsZCARD key→ compare to limit
Run inside Lua to avoid races.
Production checklist
- Key design:
ratelimit:{api}:{userId}or IP-based; combine where abuse is likely. - TTL:
PEXPIREso keys do not leak. - Clock skew: prefer Redis
TIMEif needed. - 429 responses: include
Retry-After(RFC 6585). - Observability:
rate_limit_allowed_total,rate_limit_rejected_total.
Summary
- Token bucket: great for smoothing bursts; implement atomically with Redis + Lua.
- Sliding window: fairer limits; ZSET or scripted cleanup.
- Content targeting golang rate limiting should include runnable snippets and real key names — readers copy-paste and adapt.
For a follow-up, wire the same primitives into gRPC interceptors or HTTP middleware in a dedicated post.
Related posts
JWT Authentication in Go: Access Tokens, Refresh Tokens, and Secure Storage
Sign and verify JWTs in Go; short-lived access tokens, refresh rotation, HttpOnly cookies, and common pitfalls.
gRPC vs REST: When Should You Use Which? A Comparative Guide with Go
gRPC and REST in microservices: protobuf, HTTP/2, browser constraints, and Go examples — complements our Go vs Node.js service comparison.
Building CLIs in Go: A Deep Dive into Command-Line Tools
Subcommands with Cobra, flag parsing, stdin/stdout, exit codes, cross-compilation, and testing — a practical guide to production-grade Go CLI development.