Skip to main content
The Sliding Window algorithm tracks each request individually in a Redis Sorted Set (ZSET), scored by its millisecond timestamp. On every request, entries older than now - windowSize are pruned first. This provides a true rolling window with no burst-at-boundary problem, at the cost of higher Redis memory usage.

Configuration

rate-limiter:
  use-algo: sliding-window-algo  # default
  window-size: 60      # seconds per window
  max-requests: 5      # max requests per window

Redis key format

The algorithm uses a single ZSET per user, accumulating individual request entries:
ComponentValue
Key patternrlaas:rate_limit:{userId}
Member format{currentTimeMillis}-{random}
ScoreUnix timestamp in milliseconds
TTLwindow * 2 milliseconds, extended on every request
The random suffix on each member ensures uniqueness when two requests arrive within the same millisecond. Example ZSET contents for userId=alice:
"1700000100000-0.432"  →  score 1700000100000
"1700000115000-0.871"  →  score 1700000115000
"1700000130000-0.123"  →  score 1700000130000

Lua script

The full slidingWindow.lua script executed atomically in Redis:
local key   = KEYS[1]                          -- e.g. rlaas:rate_limit:alice
local now   = tonumber(ARGV[1])                -- current time in milliseconds
local window = tonumber(ARGV[2]) * 1000        -- windowSize converted to ms
local limit = tonumber(ARGV[3])                -- max requests allowed

-- Step 1: Remove entries that have fallen outside the rolling window
redis.call("ZREMRANGEBYSCORE", key, 0, now - window)

-- Step 2: Count entries still inside the window
local count = redis.call("ZCARD", key)

if count < limit then
    -- Step 3a: Under limit — add this request and extend TTL
    local member = tostring(now) .. "-" .. math.random()
    redis.call("ZADD", key, now, member)
    redis.call("PEXPIRE", key, window * 2)
    return {1, count, window/1000}             -- allowed; ttl = windowSize in seconds
else
    -- Step 3b: At or over limit — extend TTL and deny
    redis.call("PEXPIRE", key, window * 2)
    return {0, count, window/1000}             -- denied; ttl = windowSize in seconds
end

Algorithm steps

1

Prune stale entries

ZREMRANGEBYSCORE key 0 (now - window) removes all ZSET members whose score (timestamp) is older than the current rolling window. This keeps the set bounded to only the requests that count against the current limit.
2

Count active entries

ZCARD key returns the number of entries remaining in the ZSET after pruning. This is the number of requests already made within the rolling window.
3

Allow if under limit

If count < limit, the request is allowed. A new member is added with ZADD using the current timestamp as the score, and the key TTL is extended with PEXPIRE. Returns {1, count, window/1000}.
4

Deny if at or over limit

If count >= limit, the request is denied. The TTL is still extended with PEXPIRE to keep the key alive while the user continues making requests. Returns {0, count, window/1000}.

Response fields

FieldTypeDescription
allowedbooleantrue if under limit, false if exceeded
currentCountlongNumber of requests already in the window (before this one)
ttllongThe configured window-size in seconds (returned as window/1000)
Unlike Fixed Window, the TTL returned for a Sliding Window denial is always equal to the configured window-size — it does not reflect the exact time until the oldest entry expires.

See also

Fixed Window algorithm reference

Compare the fixed window approach: lower memory use, simpler key structure, but susceptible to boundary bursts.

Build docs developers (and LLMs) love