|
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 |
6 | 3 |
|
| 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) |
7 | 32 | redis.call('ZREMRANGEBYSCORE', key, '-inf', cutoff) |
8 | 33 |
|
9 | 34 | local count = redis.call('ZCARD', key) |
10 | 35 |
|
11 | 36 | 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. |
12 | 39 | local member = tostring(now) .. '-' .. tostring(redis.call('INCR', key .. ':seq')) |
13 | 40 | redis.call('ZADD', key, now, member) |
14 | 41 |
|
|
0 commit comments