lua python 115 lines · 4 tabs

Redis-Backed Token Bucket Rate Limiter as FastAPI Middleware

Shared by codesnips Jul 2026
4 tabs
-- KEYS[1] = bucket key
-- ARGV[1] = capacity, ARGV[2] = refill_per_sec, ARGV[3] = now (float seconds)
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

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

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

local elapsed = math.max(0, now - ts)
tokens = math.min(capacity, tokens + elapsed * rate)

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

redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', now)
-- expire idle buckets once they would be fully refilled
redis.call('EXPIRE', KEYS[1], math.ceil(capacity / rate) + 1)

return { allowed, tokens }
4 files · lua, python Explain with highlit

This snippet implements API rate limiting using the token bucket algorithm, enforced as ASGI middleware in FastAPI and backed by Redis so the limit holds across multiple application instances. A token bucket models a bucket that refills at a steady rate up to a fixed capacity; each request consumes one token, and requests are rejected when the bucket is empty. Compared to fixed-window counters, this smooths traffic and allows short bursts up to the bucket's capacity without the boundary spikes fixed windows suffer from.

The core logic lives in token_bucket.lua, a Redis script that reads the stored token count and last-refill timestamp, computes how many tokens should have accrued since the last call, caps the total at capacity, and decrements one token if any remain. Running this as a Lua script is essential: Redis executes it atomically, so the read-modify-write cycle cannot interleave with concurrent requests from other workers. Doing the same in three separate GET/SET calls would create a race where two requests both see the last token and both succeed. The script also sets a TTL so idle buckets expire and do not leak keys.

In rate_limiter.py, TokenBucketLimiter loads and registers the script with register_script, then allow invokes it with the per-client key and the current time. It returns whether the request is permitted plus the remaining tokens and a retry_after hint derived from the refill rate. Passing time from Python keeps the script deterministic and testable, though production systems must ensure clocks are reasonably synchronized.

middleware.py wires this into the request lifecycle via RateLimitMiddleware, which derives a client identity (an API key header falling back to the peer IP), calls limiter.allow, and short-circuits with a 429 and Retry-After header when denied. On success it forwards the request and attaches X-RateLimit-Remaining so clients can self-throttle.

main.py shows the assembly: a Redis connection, one limiter configured with capacity and refill rate, and the middleware added to the app. Key trade-offs to weigh include choosing the identity dimension carefully to avoid punishing users behind shared NATs, and accepting Redis as a dependency in the hot path.


Related snips

Share this code

Here's the card — post it anywhere.

Redis-Backed Token Bucket Rate Limiter as FastAPI Middleware — share card
Link copied