2 min read

Redis Sliding Window Rate Limit in Practice

How to implement per-user call limits with Sliding Window and reduce false positives and misses

Redis Sliding Window Rate Limit in Practice thumbnail

Introduction

The goal of rate limits is to “maintain a normal user experience” rather than “block.” The fixed window method is easy to implement, but allows bursts at boundary points, causing false positives and omissions. In services with urgent traffic, this small difference can lead to failure.

This article covers patterns for reliably applying Redis ZSET-based Sliding Window in practice. In particular, we look at atomicity, memory management, and observation metrics together in a multi-instance API environment.

Redis Sliding Window Rate Limit 실전 커버
Wikimedia Commons 기반 무료 이미지

Problem definition

Common problems in the field include:

  • The request count is only processed as INCR + EXPIRE, so the allowable amount jumps at the window border.
  • Accuracy decreases when the number of API instances increases due to application-level locking.
  • There is a limit exceeded response, but it is not possible to track "which key was blocked and why".
  • Memory usage is not predicted because there are no Redis key cleanup rules.

The key is to simultaneously secure atomicity and operational visibility.

Key concepts

elementRecommended ChoiceReason
data structureZSET (timestamp score)Simple section deletion and count calculation
How it runsLua scriptProcess check + insert atomically
key designrl:{scope}:{id}Easy operation of multiple policies in parallel
response headerX-RateLimit-*Client Retry Strategy Association

In practice, a policy is not completed with just one policy. It is safe to separate the user, ip, token policies and apply the strictest result.

Code example 1: Sliding Window Lua script

-- KEYS[1]: rate-limit key
-- ARGV[1]: now (ms)
-- ARGV[2]: window (ms)
-- ARGV[3]: limit

local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])

redis.call("ZREMRANGEBYSCORE", key, 0, now - window)
local current = redis.call("ZCARD", key)

if current >= limit then
  local oldest = redis.call("ZRANGE", key, 0, 0, "WITHSCORES")
  local retryAfter = 0
  if oldest[2] ~= nil then
    retryAfter = window - (now - tonumber(oldest[2]))
  end
  return {0, current, retryAfter}
end

redis.call("ZADD", key, now, tostring(now))
redis.call("PEXPIRE", key, window)

return {1, current + 1, 0}

Code example 2: Node.js middleware wrapper

import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);
const windowMs = 60_000;
const limit = 120;

export async function applyUserRateLimit(userId: string) {
  const key = `rl:user:${userId}`;
  const now = Date.now();

  const [allowed, current, retryAfter] = (await redis.evalsha(
    process.env.RL_SHA!,
    1,
    key,
    now,
    windowMs,
    limit,
  )) as [number, number, number];

  return {
    allowed: allowed === 1,
    remaining: Math.max(0, limit - current),
    retryAfterMs: retryAfter,
  };
}

Architecture flow

Mermaid diagram rendering...

Tradeoffs

  • Sliding Window has high accuracy, but requires more calculations than Fixed Window.
  • Using Lua improves accuracy, but the operations team must have a script version control system.
  • The more detailed the policy, the better the user protection, but the number of debugging points increases.

Cleanup

Rate limit is both a security feature and a UX feature. When introducing Redis Sliding Window, you should not only look at algorithm accuracy, but also design header standardization and indicator collection to be effective in actual operation.

Image source

  • Cover: source link
  • License: CC BY-SA 3.0 / Author: Esquilo
  • Note: After downloading the free license image from Wikimedia Commons, it was optimized to JPG at 1600px.

Comments