Tracks available tokens in a HASH; refills at rate (e.g., 100/hour = ~0.028/sec). Allows bursts up to bucket capacity—ideal for uneven traffic patterns.
Token Bucket Lua Script
local key = KEYS[1]
local rate = tonumber(ARGV[1]) -- tokens per second
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local data = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(data[1] or capacity)
local last_refill = tonumber(data[2] or now)
local elapsed = now - last_refill
tokens = math.min(capacity, tokens + (elapsed * rate))
if tokens >= 1 then
tokens = tokens - 1
redis.call('HSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', key, 3600) -- 1hr window
return {1, tokens}
else
local ttl = redis.call('TTL', key)
return {0, ttl}
end
Node.js Middleware
const tokenBucketScript = `
local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local data = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(data[1] or capacity)
local last_refill = tonumber(data[2] or now)
local elapsed = now - last_refill
tokens = math.min(capacity, tokens + (elapsed * rate))
if tokens >= 1 then
tokens = tokens - 1
redis.call('HSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', key, 3600)
return {1, tokens}
else
local ttl = redis.call('TTL', key)
return {0, ttl}
end
`;
const tokenScript = new redis.Script(tokenBucketScript);
async function tokenBucketRateLimit(req, res, next) {
const identifier = req.ip;
const key = `tokenbucket:${identifier}`;
const RATE = 100 / 3600; // 100 tokens/hour
const CAPACITY = 10; // burst up to 10
const now = Math.floor(Date.now() / 1000);
const result = await tokenScript.eval(redis, [key], [RATE, CAPACITY, now]);
if (result[0] === 0) {
res.set('Retry-After', result[1]);
return res.status(429).json({ error: 'Rate limit exceeded' });
}
res.set('X-RateLimit-Remaining', Math.floor(result[1]));
next();
}
module.exports = tokenBucketRateLimit;
How it works:
- Refills tokens proportional to elapsed time since last check
- Consumes 1 token per request; blocks if zero
- Lua ensures atomicity; ideal for uneven traffic
- Burst capacity allows short traffic spikes without penalty