Skip to content

Commit bd48a7d

Browse files
committed
Add comments and fix formatting for redis scripts
1 parent 29930bf commit bd48a7d

2 files changed

Lines changed: 55 additions & 13 deletions

File tree

scripts/lua/sliding_window.lua

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,41 @@
1-
local key = KEYS[1]
2-
local now = tonumber(ARGV[1])
3-
local window_ms = tonumber(ARGV[2])
4-
local limit = tonumber(ARGV[3])
5-
local cutoff = now - window_ms
1+
--[[
2+
Sliding Window Rate Limiter Redis Script
63
4+
This script runs atomically inside the Redis Lua interpreter (single-threaded).
5+
No external locking is needed. No TOCTOU race is possible across service replicas,
6+
all replicas share this Redis instance as the single source of truth for decisions.
7+
8+
KEYS[1] — rate-limit key (e.g. "rl:my-api-key")
9+
ARGV[1] — current timestamp in milliseconds (Unix ms)
10+
ARGV[2] — window size in milliseconds
11+
ARGV[3] — request limit (max allowed per window)
12+
13+
Returns: 1 if the request is allowed, 0 if rejected.
14+
15+
Algorithm:
16+
1. Remove all entries with score < (now - window_ms).
17+
These timestamps are outside the sliding window — expired.
18+
2. Count remaining entries. These are requests within the active window.
19+
3. If count < limit: record this request (ZADD with score = now) and allow.
20+
4. If count >= limit: reject without recording.
21+
5. Reset the key TTL to window_ms so idle keys are cleaned up automatically.
22+
Without this, sorted sets for inactive keys persist in Redis indefinitely.
23+
]]
24+
25+
local key = KEYS[1]
26+
local now = tonumber(ARGV[1])
27+
local window_ms = tonumber(ARGV[2])
28+
local limit = tonumber(ARGV[3])
29+
local cutoff = now - window_ms
30+
31+
-- Score range: -inf to cutoff (exclusive of the active window)
732
redis.call('ZREMRANGEBYSCORE', key, '-inf', cutoff)
833

934
local count = redis.call('ZCARD', key)
1035

1136
if count < limit then
37+
-- Member must be unique within the sorted set.
38+
-- Appending a microsecond-precision suffix handles bursts at identical millisecond timestamps.
1239
local member = tostring(now) .. '-' .. tostring(redis.call('INCR', key .. ':seq'))
1340
redis.call('ZADD', key, now, member)
1441

scripts/lua/token_bucket.lua

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
1-
local key = KEYS[1]
2-
local now = tonumber(ARGV[1])
3-
local capacity = tonumber(ARGV[2])
4-
local rate = tonumber(ARGV[3])
1+
--[[
2+
Token Bucket Rate Limiter Redis Script
53
6-
local data = redis.call('HMGET', key, 'tokens', 'last_refill')
7-
local tokens = tonumber(data[1]) or capacity
4+
Kept alongside sliding_window.lua to highlights the tradeoffs in docs/tradeoffs.md.
5+
6+
KEYS[1] — rate-limit key
7+
ARGV[1] — current timestamp in seconds (float precision)
8+
ARGV[2] — bucket capacity (max tokens)
9+
ARGV[3] — refill rate (tokens per second)
10+
11+
Returns: 1 if allowed, 0 if rejected.
12+
]]
13+
14+
local key = KEYS[1]
15+
local now = tonumber(ARGV[1])
16+
local capacity = tonumber(ARGV[2])
17+
local rate = tonumber(ARGV[3])
18+
19+
local data = redis.call('HMGET', key, 'tokens', 'last_refill')
20+
local tokens = tonumber(data[1]) or capacity
821
local last_refill = tonumber(data[2]) or now
922

10-
local elapsed = math.max(0, now - last_refill)
11-
local new_tokens = math.floor(capacity, tokens + elapsed * rate)
23+
-- Refill tokens proportional to elapsed time since last request
24+
local elapsed = math.max(0, now - last_refill)
25+
local new_tokens = math.floor(capacity, tokens + elapsed * rate)
1226

1327
if new_tokens < 1 then
1428
return 0
1529
end
1630

31+
-- Consume one token and persist updated state
1732
redis.call('HMSET', key, 'tokens', new_tokens - 1, 'last_refill', now)
1833
redis.call('EXPIRE', key, math.ceil(capacity / rate) + 1)
1934

0 commit comments

Comments
 (0)