CodePark/ questions / Rate limiting & counters
START PRACTICINGPRACTICE
Backend question · Redis & caching › Rate limiting & counters

Implement a fixed-window rate limiter (100 requests per minute per user) with Redis. What atomicity issue must you watch for?

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:

  • INCR is atomic: Redis runs it as one step, so two requests can't both read the same old value. If the key is missing, INCR creates 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 MULTI block, as shown.
  • Use a small Lua script, which Redis runs as one atomic step.
  • Use EXPIRE key 60 NX (Redis 7). NX means 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 INCR on 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 ... NX to keep them together.
  • It is cheap, but it allows bursts at window boundaries.