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

How would you build a sliding-window rate limiter with a sorted set? What are its costs?

const now = Date.now();
const key = "rl:" + userId;
const results = await redis
  .multi()
  .zremrangebyscore(key, 0, now - windowMs)
  .zadd(key, now, now + "-" + randomUUID())
  .zcard(key)
  .pexpire(key, windowMs)
  .exec();
const count = results[2][1] as number;
if (count > limit) throw new TooManyRequestsException();

Keep one sorted set per user. Each request is a member of the set, and its score is the request's timestamp. A sorted set is a Redis set where every member has a number (the score) and Redis keeps members ordered by that number.

On every request, do these steps atomically (as one step that nothing can interrupt):

  • Remove entries older than the window with ZREMRANGEBYSCORE.
  • Add the current request.
  • Count the entries with ZCARD.
  • Refresh the key's TTL.

If the count is over the limit, reject the request. Give each member a unique suffix. Otherwise two requests in the same millisecond become one member.

This gives an exact rolling window, with no bursts at window boundaries.

Costs and caveats:

  • Memory: the set holds one entry per request in the window. A limit of 10,000 per hour stores up to 10,000 entries per user.
  • The version above also records rejected requests. So a client that keeps sending requests stays blocked. A Lua script can check the count first and only add the request when it is allowed.

A cheaper approximation is the sliding window counter. It uses fixed-window counters, and gives the previous window's count a weight based on how much of it still overlaps the rolling window.

  • Use one sorted set per user, with the timestamp as the score.
  • Trim old entries, add, count and set the TTL as one atomic step.
  • Make members unique so requests in the same millisecond are not merged.
  • The window is exact, but memory grows with the limit.
  • Use a Lua script to avoid counting rejected requests.