lua go 115 lines · 3 tabs

Redis-Backed Token Bucket Rate Limiter with Lua Atomic Refill in Go

Shared by codesnips Aug 2026
3 tabs
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local ttl = tonumber(ARGV[4])

local bucket = redis.call('HMGET', key, 'tokens', 'ts')
local tokens = tonumber(bucket[1])
local ts = tonumber(bucket[2])

if tokens == nil then
  tokens = capacity
  ts = now
end

-- lazy refill: accrue tokens for the elapsed interval
local elapsed = math.max(0, now - ts) / 1000.0
tokens = math.min(capacity, tokens + elapsed * rate)

local allowed = 0
if tokens >= 1 then
  tokens = tokens - 1
  allowed = 1
end

redis.call('HMSET', key, 'tokens', tokens, 'ts', now)
redis.call('PEXPIRE', key, ttl)

return { allowed, math.floor(tokens) }
3 files · lua, go Explain with highlit

This snippet implements a distributed token bucket rate limiter for API quota tracking, where the refill logic runs as an atomic Lua script inside Redis rather than as a background sweep job. The token bucket pattern models a bucket that holds up to capacity tokens and refills at a steady refillRate per second; each request consumes one token, and requests are rejected when the bucket is empty. Refilling lazily on each request — computing how many tokens should have accrued since the last access — avoids the classic pitfall of running a scheduler that touches millions of idle keys, and keeps the whole check-and-decrement operation race-free across many concurrent processes.

In bucket.lua, the script reads the stored tokens and ts (last refill timestamp) from a hash, calculates elapsed time against now, and adds elapsed * rate tokens capped at capacity. If at least one token remains it decrements and returns 1 (allowed) plus the current count; otherwise it returns 0. Because Redis executes Lua atomically, no other client can interleave between the read and the write, which is what makes the refill scheduler correct without locks. The script also sets a TTL so abandoned buckets self-expire, preventing unbounded key growth.

In limiter.go, the Limiter type loads the script once via redis.NewScript so Redis can cache it by SHA and invoke it with EVALSHA. Allow passes the four tuning parameters and the server-side time.Now().UnixMilli() as arguments, keeping every node's clock decision consistent by delegating time to the caller while state lives in Redis. The returned Result carries whether the request was permitted and the remaining tokens so callers can surface quota headers.

In middleware.go, RateLimit wires the limiter into an http.Handler, keying the bucket by client IP so each caller gets an independent quota. It writes X-RateLimit-Remaining and, on rejection, a Retry-After header plus a 429 status. A notable trade-off of lazy refill is that clock skew or a Redis failover can momentarily shift accrual, so the code fails open on Redis errors to avoid turning a cache blip into an outage. This design suits per-user or per-IP API quotas that must hold under horizontal scaling.


Related snips

Share this code

Here's the card — post it anywhere.

Redis-Backed Token Bucket Rate Limiter with Lua Atomic Refill in Go — share card
Link copied