lua javascript 137 lines · 3 tabs

Token-Bucket Rate Limiting Middleware for Express with Per-Route Config

Shared by codesnips Aug 2026
3 tabs
-- KEYS[1] = bucket key
-- ARGV: capacity, refillPerSec, now(ms), cost, ttl(sec)
local capacity      = tonumber(ARGV[1])
local refill        = tonumber(ARGV[2])
local now           = tonumber(ARGV[3])
local cost          = tonumber(ARGV[4])
local ttl           = tonumber(ARGV[5])

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) / 1000.0
tokens = math.min(capacity, tokens + (elapsed * refill))

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

redis.call('HMSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('EXPIRE', KEYS[1], ttl)

-- ms until enough tokens accrue for one more request
local deficit = math.max(0, cost - tokens)
local retryMs = 0
if refill > 0 then
  retryMs = math.ceil((deficit / refill) * 1000)
end

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

This snippet implements a token-bucket rate limiter as reusable Express middleware backed by Redis, with limits configured per route rather than globally. The token-bucket algorithm models each client's quota as a bucket that refills at a fixed rate up to a maximum capacity; each request removes one token, and requests are rejected when the bucket is empty. Unlike a fixed-window counter, a bucket allows short bursts (up to capacity) while still enforcing a steady long-run rate (refillPerSec), which maps naturally onto how real API traffic behaves.

The core lives in tokenBucket.lua, a Redis script that reads the stored token count and last-refill timestamp, computes how many tokens have accrued since the last call, clamps to capacity, and atomically either debits a token or reports failure. Running the refill-and-debit as a single Lua script is what makes this correct under concurrency: no other client can interleave between the read and the write, so two simultaneous requests can never both consume the last token. The script also sets a TTL so idle buckets expire and Redis memory stays bounded.

In rateLimiter.js, the rateLimiter factory takes per-route options and returns a standard (req, res, next) middleware. It derives a key (by default the client IP scoped to the route), invokes the script via EVALSHA, and on rejection responds with 429 plus the standard RateLimit-* and Retry-After headers so well-behaved clients can back off. The keyGenerator option lets a route key by API token or user id instead of IP, which is important behind proxies where many users share an address. The script is loaded once and cached by SHA to avoid shipping its body on every call.

In routes.js, different endpoints get different budgets: a login route is throttled tightly to blunt credential-stuffing, while a read endpoint gets a generous burst. Because each middleware instance is independent, limits compose cleanly and stay declarative next to the route. A key pitfall to watch is trusting req.ip without configuring trust proxy, and choosing a cost per request when some operations are heavier than others.


Related snips

Share this code

Here's the card — post it anywhere.

Token-Bucket Rate Limiting Middleware for Express with Per-Route Config — share card
Link copied