Mert Tosun
← Posts
Rate Limiting with Redis: Token Bucket and Sliding Window in Go

Rate Limiting with Redis: Token Bucket and Sliding Window in Go

Mert TosunGo

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:

  1. Sorted set: add request timestamp as score; remove old entries with ZREMRANGEBYSCORE; ZCARD for count.
  2. Approximate algorithms for very high QPS.

ZSET flow

  1. ZADD key now uniqueMember
  2. ZREMRANGEBYSCORE key 0 now-windowMs
  3. ZCARD 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: PEXPIRE so keys do not leak.
  • Clock skew: prefer Redis TIME if 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.