How to Build a Custom Rate Limiter from Scratch with Redis and Node.js


Rate limiting is an essential defense layer for any backend API. While simple fixed-window algorithms (like resetting counts every minute) are easy to code, they are vulnerable to traffic spikes at window boundaries.
The Sliding Window Log pattern eliminates boundary spikes by tracking individual request timestamps per user in Redis using sorted sets (ZSET).


Follow this step-by-step tutorial to implement a production-ready sliding window rate limiter:


Step 1: Set Up the Redis Sorted Set Strategy
Instead of a simple counter key, store each request timestamp inside a Redis ZSET where:
Key: rate_limit:{user_id_or_ip}
Member: Unique Request ID / Timestamp
Score: Epoch Timestamp (milliseconds)


Step 2: Clean Up Old Request Logs
When a request arrives at your API middleware, immediately remove all timestamps older than the allowed window limit (e.g., older than 60 seconds ago):
JavaScript
const windowSizeInMs = 60 * 1000;
const now = Date.now();
const clearBefore = now - windowSizeInMs;
// Remove expired entries from the sorted set
await redis.zremrangebyscore(userKey, 0, clearBefore);


Step 3: Count Requests Within the Active Window
Fetch the current number of valid requests remaining in the sorted set for the current window:
JavaScript
const currentRequestCount = await redis.zcard(userKey);
if (currentRequestCount >= MAX_ALLOWED_REQUESTS) {
// Exceeded rate limit
return res.status(429).json({
error: 'Too Many Requests',
retryAfter: 60


Step 4: Record the Current Request & Set Expiration
If the request is within the limit, add the current timestamp to Redis and refresh the key's TTL to prevent idle memory leaks:
JavaScript
// Add current request
await redis.zadd(userKey, now, `${now}-${Math.random()}`);
// Set key expiration to prevent stale data buildup
await redis.pexpire(userKey, windowSizeInMs);
next(); // Continue to API handler


Step 5: Wrap Execution in an Atomic Redis Multi Transaction
To eliminate race conditions between reading and writing to Redis across concurrent server nodes, execute ZREMRANGEBYSCORE, ZCARD, ZADD, and PEXPIRE within a single redis.multi() transaction block or a custom Lua script.


Key Takeaways
Prefer Sliding Windows: Avoid boundary-burst vulnerabilities inherent in fixed-window algorithms.
Keep Data Lean: Always auto-expire Redis keys using PEXPIRE to maintain minimal memory footprint.
Ensure Atomicity: Use Redis Lua scripts or multi transactions to guarantee thread safety during high concurrency.


CTA
How do you handle API throttling and load protection in your stack? Join Developers & Coding to discuss backend design patterns, benchmark database solutions, and build resilient infrastructure with engineers around the globe.
How to Build a Custom Rate Limiter from Scratch with Redis and Node.js Rate limiting is an essential defense layer for any backend API. While simple fixed-window algorithms (like resetting counts every minute) are easy to code, they are vulnerable to traffic spikes at window boundaries. The Sliding Window Log pattern eliminates boundary spikes by tracking individual request timestamps per user in Redis using sorted sets (ZSET). Follow this step-by-step tutorial to implement a production-ready sliding window rate limiter: Step 1: Set Up the Redis Sorted Set Strategy Instead of a simple counter key, store each request timestamp inside a Redis ZSET where: Key: rate_limit:{user_id_or_ip} Member: Unique Request ID / Timestamp Score: Epoch Timestamp (milliseconds) Step 2: Clean Up Old Request Logs When a request arrives at your API middleware, immediately remove all timestamps older than the allowed window limit (e.g., older than 60 seconds ago): JavaScript const windowSizeInMs = 60 * 1000; const now = Date.now(); const clearBefore = now - windowSizeInMs; // Remove expired entries from the sorted set await redis.zremrangebyscore(userKey, 0, clearBefore); Step 3: Count Requests Within the Active Window Fetch the current number of valid requests remaining in the sorted set for the current window: JavaScript const currentRequestCount = await redis.zcard(userKey); if (currentRequestCount >= MAX_ALLOWED_REQUESTS) { // Exceeded rate limit return res.status(429).json({ error: 'Too Many Requests', retryAfter: 60 Step 4: Record the Current Request & Set Expiration If the request is within the limit, add the current timestamp to Redis and refresh the key's TTL to prevent idle memory leaks: JavaScript // Add current request await redis.zadd(userKey, now, `${now}-${Math.random()}`); // Set key expiration to prevent stale data buildup await redis.pexpire(userKey, windowSizeInMs); next(); // Continue to API handler Step 5: Wrap Execution in an Atomic Redis Multi Transaction To eliminate race conditions between reading and writing to Redis across concurrent server nodes, execute ZREMRANGEBYSCORE, ZCARD, ZADD, and PEXPIRE within a single redis.multi() transaction block or a custom Lua script. Key Takeaways Prefer Sliding Windows: Avoid boundary-burst vulnerabilities inherent in fixed-window algorithms. Keep Data Lean: Always auto-expire Redis keys using PEXPIRE to maintain minimal memory footprint. Ensure Atomicity: Use Redis Lua scripts or multi transactions to guarantee thread safety during high concurrency. CTA How do you handle API throttling and load protection in your stack? Join Developers & Coding to discuss backend design patterns, benchmark database solutions, and build resilient infrastructure with engineers around the globe.
0 Comentários 0 Compartilhamentos 1KB Visualizações 0 Anterior