Project
go-redis-token-bucket
shippedA distributed rate limiter: one limit across N nodes, enforced by an atomic Lua script in Redis.
go · redis · lua
What it is
A minimal demonstration of a distributed token bucket rate limiter built with Go, Redis and a Lua script. Four identical API nodes share a single Redis instance, so a client is limited to its quota across the whole cluster, not per node.
It is a learning exercise, not a library, and the README is explicit that it is a toy: the client identity comes from an unauthenticated X-Client-ID header, and there is no TLS, authentication or Redis replication.
Architecture
┌──────────┐ │ client │ 1000 concurrent requests, │ (Go) │ randomly spread over 4 ports └────┬─────┘ ┌───────────┬───┴───────┬───────────┐ ▼ ▼ ▼ ▼ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ node1 │ │ node2 │ │ node3 │ │ node4 │ │ :8081 │ │ :8082 │ │ :8083 │ │ :8084 │ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ └───────────┴─────┬─────┴───────────┘ ▼ ┌─────────────┐ │ Redis │ one bucket per client: │ │ ratelimit:global:<client-id> └─────────────┘The nodes hold no state. Each bucket is a Redis hash with tokens and last_refill, created full on a client’s first request and refilled lazily when a request arrives. A Lua script does read, refill, compare, decrement and write as one atomic operation, called by SHA1 via EVALSHA. The bucket holds 20 tokens (the burst) and refills at 1 per second (the sustained rate). A rejected request gets 429 with Retry-After.
What I learned
- The check-and-decrement has to be atomic. Without it, two nodes read the same token count and both allow the request: a read-modify-write race that would happen constantly under this load.
- A token bucket separates sustained rate from burst, which a naive fixed-window counter cannot express.
- The shared quota holds across nodes. In a typical run of 1000 requests, 16 got
200 OKand 984 got429 Too Many Requests, spread across all four nodes. Per-node buckets would have allowed4 × 20 = 80. - Failing closed is a policy decision. If Redis is down every request gets a
500; production systems usually fail open or fall back to a local limit.