# Hand-rolled token bucket on Upstash Redis with Lua
The SDKs token bucket may not fit: custom refill curves, per-tenant
burst budgets, or cost attribution per consume. A Lua script gives you
atomic check-and-consume in one round trip.
## The script
Store tokens, capacity, refill rate, and last timestamp in a hash per
identifier. The script computes elapsed time, refills, and consumes
atomically:
local key [your value]
local now = tonumber(ARGV[1])
local requested = tonumber(ARGV[2])
local data = redis.call('HMGET', key, 'tokens', 'ts')
local tokens = tonumber(data[1]) or CAPACITY
local ts = tonumber(data[2]) or now
local refill = math.floor((now - ts) / 1000 * REFILL_RATE)
tokens = math.min(CAPACITY, tokens + refill)
if tokens >= requested then
redis.call('HSET', key, 'tokens', tokens - requested, 'ts', now)
redis.call('PEXPIRE', key, 60000)
return 1
else
redis.call('HSET', key, 'tokens', tokens, 'ts', now)
redis.call('PEXPIRE', key, 60000)
return 0
end
Bake your CAPACITY and REFILL_RATE into the script or pass them as
ARGV. Keep PEXPIRE so idle buckets disappear instead of leaking
memory.
## Run it
Over REST, send EVAL with the script and keys. Over the SDK, use
eval with the same arguments. One network round trip, atomic
consume, no race between check and decrement.
## Cost
One EVAL plus the commands inside it, per check. That matches the
SDKs token-bucket cost profile (EVAL, HMGET, HSET, PEXPIRE) without
the SDKs opinion about refill math.
## Verify
Burst-test: fire 2x capacity in a tight loop and confirm exactly
capacity requests return 1. Wait one refill interval and confirm
tokens reappear.