Secure Node.js Rate Limiting with Sliding Window
Rate limiting is a critical security measure used to protect APIs from abuse, brute-force attacks, and Denial of Service (DoS) attempts. This article provides a practical guide on how to implement a secure rate limiter in Node.js using the sliding window log algorithm. We will explore the mechanics of this algorithm and walk through a production-ready implementation using Redis to ensure atomic operations and distributed accuracy.
Understanding the Sliding Window Log Algorithm
Unlike the fixed window algorithm, which resets at specific time intervals and can allow a burst of traffic at the window boundaries, the sliding window log algorithm tracks the exact timestamp of every request.
When a user makes a request, the algorithm: 1. Deletes all logged timestamps that are older than the defined sliding window limit. 2. Adds the current request’s timestamp to the log. 3. Calculates the total number of remaining logs. 4. Approves the request if the count is below the limit, or rejects it if it exceeds the threshold.
To implement this securely and efficiently in Node.js, we use
Redis and its Sorted Sets (ZSET). This
prevents memory exhaustion on the application server and ensures
accurate tracking across multiple server instances.
Step-by-Step Implementation in Node.js
1. Prerequisites
First, install the necessary dependencies. You will need
express for the web server and ioredis for
connecting to Redis.
npm install express ioredis2. The Rate Limiter Logic
The sliding window log is managed atomically using a Redis
transaction (multi). We use the client’s IP address (or API
key) as the key, the timestamp as both the score and the member in the
Sorted Set.
Create a file named rateLimiter.js:
const Redis = require('ioredis');
const redis = new Redis(); // Connects to localhost:6379 by default
/**
* Checks if a request exceeds the allowed rate limit.
* @param {string} key - Unique identifier (e.g., IP address or API key).
* @param {number} limit - Maximum number of allowed requests.
* @param {number} windowInSeconds - Time window size in seconds.
* @returns {Promise<boolean>} - True if rate limited, false otherwise.
*/
async function isRateLimited(key, limit, windowInSeconds) {
const now = Date.now();
const windowStart = now - (windowInSeconds * 1000);
const transaction = redis.multi();
// 1. Remove elements (timestamps) older than the current window start
transaction.zremrangebyscore(key, 0, windowStart);
// 2. Add the current timestamp to the sorted set
transaction.zadd(key, now, now);
// 3. Count the remaining elements in the set
transaction.zcard(key);
// 4. Set an expiration on the key to automatically clean up inactive users
transaction.expire(key, windowInSeconds);
const results = await transaction.exec();
// The result of zcard is the third command in the multi transaction (index 2)
const requestCount = results[2][1];
return requestCount > limit;
}
module.exports = isRateLimited;3. Integrating with Express Middleware
Now, let’s create an Express middleware to intercept incoming requests and apply the rate limiting logic.
Create a file named server.js:
const express = require('express');
const isRateLimited = require('./rateLimiter');
const app = express();
// Enable trust proxy if your app is behind a reverse proxy (like Nginx or Cloudflare)
app.set('trust proxy', true);
const rateLimitMiddleware = async (req, res, next) => {
const clientIp = req.ip;
const LIMIT = 100; // Max 100 requests
const WINDOW = 60; // Per 60 seconds
try {
const limited = await isRateLimited(`rate_limit:${clientIp}`, LIMIT, WINDOW);
if (limited) {
return res.status(429).json({
error: 'Too Many Requests',
message: 'You have exceeded your request limit. Please try again later.'
});
}
next();
} catch (error) {
console.error('Rate limiter error:', error);
// Fail-open or fail-closed based on your security policy
next();
}
};
app.get('/api/resource', rateLimitMiddleware, (req, res) => {
res.json({ message: 'Success! You accessed the protected resource.' });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});Security and Performance Considerations
While highly accurate, the sliding window log algorithm has specific trade-offs:
- Memory Usage: Since every request timestamp is
saved in Redis, memory usage scales linearly with the number of
requests. If an attacker floods the server, the Sorted Set can grow
rapidly. To mitigate this, ensure your Redis instance is configured with
a proper eviction policy (such as
volatile-lru) and that key expirations (EXPIRE) are strictly applied. - IP Spoofing Protection: In production, ensure
app.set('trust proxy', true)is configured correctly to prevent clients from spoofing their IP address via customX-Forwarded-Forheaders. - Distributed Environments: Using Redis ensures that multiple application instances share the same rate-limiting state, preventing users from bypassing limits by rotating between different server instances.