Sliding Window Rate Limiting — Complete Implementation Guide
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
Not cleaning up old timestamps -- Sliding window arrays grow indefinitely without cleanup. Always remove expired entries before checking.
Using timestamps without uniqueness -- Two requests arriving at the same millisecond create duplicate members in sorted sets. Append a unique suffix.
High memory usage for high-traffic endpoints -- Each request stores a timestamp. For 10K req/s endpoints, use the weighted approximation instead.
Not handling clock skew in distributed sorted sets -- Servers with different clocks write timestamps in the future or past. Use NTP-synchronized clocks.
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
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.
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.
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).
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
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