Skip to content

Sliding Window Rate Limiting — Complete Implementation Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Sliding Window Rate Limiting. We cover key concepts, practical examples, and best practices to help you master this topic.

Sliding window rate limiting tracks requests in a rolling time window, providing precise rate enforcement without the boundary spikes of fixed window algorithms.

What You'll Learn

By the end of this tutorial, you will implement sliding window rate limiting using Redis sorted sets, understand the time complexity trade-offs, and configure precise rate limits.

Why It Matters

Sliding window eliminates the fixed window boundary problem, making it ideal for security-critical endpoints. DodaTech uses sliding window for authentication and payment API rate limiting.

Real-World Use

Doda Browser's login endpoint uses sliding window rate limiting with a limit of 5 attempts per 15 minutes, preventing brute force attacks with no boundary window for attackers to exploit.

Sliding Window Learning Path

flowchart LR
  A[Fixed Window] --> B[Sliding Window]
  B --> C[Sorted Set Implementation]
  C --> D[Redis Optimization]
  B --> E{You Are Here}
  style E fill:#f90,color:#fff

Sliding Window with Array

The simplest sliding window implementation stores timestamps in an array and removes expired entries on each request.

const express = require("express");
const app = express();

const requestLog = new Map();

function slidingWindow(req, res, next) {
  const key = req.ip;
  const windowMs = 60000;
  const maxRequests = 10;
  const now = Date.now();

  if (!requestLog.has(key)) {
    requestLog.set(key, []);
  }

  const timestamps = requestLog.get(key);

  while (timestamps.length > 0 && timestamps[0] < now - windowMs) {
    timestamps.shift();
  }

  if (timestamps.length >= maxRequests) {
    const oldest = timestamps[0];
    const retryAfter = Math.ceil((oldest + windowMs - now) / 1000);
    return res.status(429).json({
      error: "Too many requests",
      retryAfter
    });
  }

  timestamps.push(now);
  next();
}

app.use(slidingWindow);
app.get("/", (req, res) => res.json({ ok: true }));
app.listen(3000);

Expected behavior: At most 10 requests in any rolling 60-second window. The retryAfter tells the client exactly when they can make the next request.

Redis Sorted Set Implementation

For distributed deployments, Redis sorted sets store timestamps as scores and members, providing efficient range queries across all server instances.

const Redis = require("ioredis");
const redis = new Redis();

async function redisSlidingWindow(req, res, next) {
  const key = `sw:${req.ip}`;
  const windowMs = 60000;
  const maxRequests = 10;
  const now = Date.now();
  const member = `${req.ip}:${now}:${Math.random()}`;

  const result = await redis.eval(`
    local key = KEYS[1]
    local now = tonumber(ARGV[1])
    local window = tonumber(ARGV[2])
    local max = tonumber(ARGV[3])
    local member = ARGV[4]

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

    if count < max then
      redis.call("ZADD", key, now, member)
      redis.call("EXPIRE", key, math.ceil(window / 1000))
      return {1, count + 1, max}
    else
      local oldest = redis.call("ZRANGE", key, 0, 0, "WITHSCORES")
      return {0, count, max, oldest[2]}
    end
  `, 1, key, now, windowMs, maxRequests, member);

  if (result[0] === 0) {
    const retryAfter = Math.ceil((parseInt(result[3]) + windowMs - now) / 1000);
    return res.status(429).json({
      error: "Too many requests",
      retryAfter
    });
  }

  next();
}

Sliding Window with Weighted Approximation

For high-traffic endpoints, a memory-efficient approximation uses the previous window's count weighted by elapsed time, avoiding per-request storage.

function weightedSlidingWindow(req, res, next) {
  const key = req.ip;
  const windowMs = 60000;
  const maxRequests = 10;
  const now = Date.now();

  const currentWindow = Math.floor(now / windowMs);
  const previousWindow = currentWindow - 1;

  const currentKey = `weighted:${key}:${currentWindow}`;
  const previousKey = `weighted:${key}:${previousWindow}`;

  const previousCount = counters.get(previousKey) || 0;
  const currentCount = (counters.get(currentKey) || 0) + 1;
  counters.set(currentKey, currentCount);

  const windowPosition = (now - currentWindow * windowMs) / windowMs;
  const weightedPrevious = previousCount * (1 - windowPosition);

  if (currentCount + weightedPrevious > maxRequests) {
    counters.set(currentKey, currentCount - 1);
    return res.status(429).json({ error: "Too many requests" });
  }

  next();
}

Common Mistakes

  1. Not cleaning up old timestamps -- Sliding window arrays grow indefinitely without cleanup. Always remove expired entries before checking.

  2. Using timestamps without uniqueness -- Two requests arriving at the same millisecond create duplicate members in sorted sets. Append a unique suffix.

  3. High memory usage for high-traffic endpoints -- Each request stores a timestamp. For 10K req/s endpoints, use the weighted approximation instead.

  4. Not handling clock skew in distributed sorted sets -- Servers with different clocks write timestamps in the future or past. Use NTP-synchronized clocks.

  5. Storing client IP in the sorted set member -- Members must be unique per request. Include a random value or counter in the member string.

Practice Questions

  1. How does sliding window eliminate the boundary problem? It tracks requests in a rolling time window based on each request's actual timestamp, not fixed window boundaries.

  2. What is the memory cost of sliding window per client? Each request stores one timestamp. For 100 requests per minute per client, that is 100 timestamps per client.

  3. When should you use the weighted approximation instead of exact tracking? When memory is constrained or traffic is very high (10K+ requests per second per client).

  4. Challenge: Implement a sliding window that supports different limits for different HTTP methods.

function methodSlidingWindow(req, res, next) {
  const methodLimits = { GET: 100, POST: 20, DELETE: 5 };
  const limit = methodLimits[req.method] || 10;
  // Apply sliding window logic with per-method limit
}

FAQ

Is sliding window worth the extra complexity?

For security-critical endpoints, yes. For general API rate limiting, fixed window is often sufficient.

How does sliding window compare to token bucket?

Sliding window is more precise. Token bucket allows more burst. Both are valid choices depending on requirements.

Can sliding window handle billions of requests?

Use the weighted approximation for high volume. Exact tracking with sorted sets becomes expensive beyond millions of entries.

How do I choose between sliding window and sliding log?

Sliding window is the more common name. Sliding log stores individual entries. They are essentially the same algorithm.

What happens if Redis goes down?

Rate limiting stops working. Requests may exceed limits until Redis recovers. Implement a fallback memory limiter for critical paths.

Mini Project

Build a sliding window rate limiter with Redis sorted sets, per-route configuration, and graceful degradation when Redis is unavailable.

const Redis = require("ioredis");
const redis = new Redis({ enableOfflineQueue: false });
const memoryFallback = new Map();

async function rateLimit(req, res, next) {
  const key = `sw:${req.ip}`;
  const windowMs = 60000;
  const max = parseInt(req.headers["x-rate-limit"] || "10");

  try {
    const now = Date.now();
    const member = `${now}:${Math.random().toString(36).slice(2)}`;

    const result = await redis.eval(`
      local k = KEYS[1]
      local n = tonumber(ARGV[1])
      local w = tonumber(ARGV[2])
      local m = tonumber(ARGV[3])
      local mb = ARGV[4]

      redis.call("ZREMRANGEBYSCORE", k, 0, n - w)
      local c = redis.call("ZCARD", k)

      if c < m then
        redis.call("ZADD", k, n, mb)
        redis.call("EXPIRE", k, 120)
        return {1, c, m}
      end
      return {0, c, m}
    `, 1, key, now, windowMs, max, member);

    if (result[0] === 0) {
      return res.status(429).json({ error: "Rate limit exceeded" });
    }
  } catch (err) {
    return fallbackRateLimit(req, res, next, max);
  }

  next();
}

function fallbackRateLimit(req, res, next, max) {
  const key = req.ip;
  const now = Date.now();
  if (!memoryFallback.has(key)) memoryFallback.set(key, []);
  const timestamps = memoryFallback.get(key).filter(t => now - t < 60000);
  if (timestamps.length >= max) return res.status(429).json({ error: "Rate limit exceeded" });
  timestamps.push(now);
  memoryFallback.set(key, timestamps);
  next();
}

What's Next

Now that you understand sliding window rate limiting, explore implementing rate limiting in Express applications. Then learn about using Redis for production rate limiting.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro