lua java 125 lines · 3 tabs

Token Bucket Rate Limiting in a Servlet Filter with Redis and Lua

Shared by codesnips Jul 2026
3 tabs
-- KEYS[1] = bucket key
-- ARGV[1] = capacity, ARGV[2] = refill_rate (tokens/sec)
-- ARGV[3] = now_ms, ARGV[4] = requested tokens
local capacity    = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now_ms      = tonumber(ARGV[3])
local requested   = tonumber(ARGV[4])

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

if tokens == nil then
  tokens = capacity
  last_refill = now_ms
end

local elapsed = math.max(0, now_ms - last_refill) / 1000.0
tokens = math.min(capacity, tokens + (elapsed * refill_rate))

local allowed = 0
local retry_after_ms = 0
if tokens >= requested then
  tokens = tokens - requested
  allowed = 1
else
  local deficit = requested - tokens
  retry_after_ms = math.ceil((deficit / refill_rate) * 1000)
end

redis.call('HSET', KEYS[1], 'tokens', tokens, 'last_refill', now_ms)
-- expire idle buckets after they would fully refill
redis.call('PEXPIRE', KEYS[1], math.ceil((capacity / refill_rate) * 1000) + 1000)

return { allowed, retry_after_ms }
3 files · lua, java Explain with highlit

This snippet implements per-client API throttling using the token bucket algorithm, applied as a Servlet Filter that sits in front of protected endpoints. A token bucket models a fixed-capacity reservoir that refills at a steady rate: each request removes one token, and requests are only allowed when at least one token remains. Unlike a fixed window counter, the bucket allows short bursts (up to the capacity) while still enforcing a sustained average rate, which makes it a good fit for real-world API traffic that arrives unevenly.

The core state lives in Redis because rate limits must be shared across every application instance behind a load balancer. RateLimiterLua holds the atomic decision logic as an embedded Lua script. Running the refill-and-consume calculation server-side matters: reading the bucket, computing the refill, and decrementing must happen as one indivisible operation, otherwise two concurrent requests could both read the same token count and both succeed. Redis executes a Lua script atomically, so the check-and-decrement becomes a single round trip with no race window. The script derives the number of tokens to add from the elapsed time since last_refill, clamps the total to capacity, and returns both whether the request is allowed and how long until the next token is available.

In TokenBucketRateLimiter, the Java side wraps the script with EVALSHA for efficiency, passing the refillRate, capacity, and current time as arguments. It returns a small Decision record carrying the allow flag and a suggested Retry-After value. Keys are namespaced per client so buckets never collide, and Redis TTLs let idle buckets expire on their own without a separate cleanup job.

Finally, RateLimitFilter bridges HTTP and the limiter. It extracts a client identity (an API key header, falling back to remote address), calls tryAcquire, and on rejection writes a 429 Too Many Requests with a Retry-After header rather than passing the request down the chain. Allowed requests continue via chain.doFilter. Placing this logic in a filter keeps throttling orthogonal to business code and lets it protect many endpoints at once. A subtle trade-off worth noting: identifying clients by IP is spoofable and breaks behind shared NAT, so production systems usually prefer authenticated keys.


Related snips

Share this code

Here's the card — post it anywhere.

Token Bucket Rate Limiting in a Servlet Filter with Redis and Lua — share card
Link copied