const windowId = Math.floor(Date.now() / 60000);
const key = "rl:" + userId + ":" + windowId;
const results = await redis
.multi()
.incr(key)
.expire(key, 60)
.exec();
const count = results[0][1] as number;
if (count > 100) throw new TooManyRequestsException();
Keep one counter key per user per time window, and run INCR on it for every request. For example, put the current minute number in the key name.
How it works:
INCRis atomic: Redis runs it as one step, so two requests can't both read the same old value. If the key is missing,INCRcreates it with the value 1.- The key gets a TTL (time to live), so old windows delete themselves.
- If the count is over the limit (100), reject the request with status 429 (Too Many Requests).
The trap: a common version runs INCR, then runs EXPIRE as a separate call only when the count is 1. If the process crashes between these two calls, the key never gets a TTL. It never expires, and the user stays blocked forever.
Fixes:
- Send both commands in a
MULTIblock, as shown. - Use a small Lua script, which Redis runs as one atomic step.
- Use
EXPIRE key 60 NX(Redis 7).NXmeans only the first call sets the TTL.
This approach is cheap: one key per user. But it allows bursts at window boundaries. For example, a user can send 100 requests at the end of one minute and 100 more at the start of the next minute.
- Use one key per user per window, and run
INCRon each request. - Give the key a TTL so old windows disappear.
- A crash must not be able to happen between INCR and EXPIRE.
- Use MULTI, a Lua script, or
EXPIRE ... NXto keep them together. - It is cheap, but it allows bursts at window boundaries.