Why Your API Needs Rate Limiting
Exposing a public API without rate limiting is like leaving your front door wide open in a busy city. Malicious actors can overwhelm your server with requests, or a bug in a client-side loop could unintentionally spike your infrastructure costs. Rate limiting ensures that your service remains available for everyone by capping the number of requests a single user or IP address can make within a specific timeframe.
Choosing Redis for Rate Limiting
While you can implement simple rate limiting in memory using a JavaScript object, it fails in production environments where you have multiple server instances. Redis is the ideal tool for this because it is extremely fast, supports atomic operations, and acts as a centralized data store for all your application nodes.
The Sliding Window Strategy
Instead of a simple fixed-window counter (which can be bypassed by spiking requests at the edge of a time window), we will use a sliding window approach. This provides a more consistent experience. We will use Redis MULTI commands to ensure our operations are atomic.
Step 1: Setting Up the Middleware
First, ensure you have ioredis and express installed. Below is a practical implementation of a rate-limiting middleware:
const Redis = require("ioredis");
const redis = new Redis();
const rateLimiter = async (req, res, next) => {
const ip = req.ip;
const key = `rate_limit:${ip}`;
const limit = 100; // Max requests
const windowSize = 60; // 60 seconds
try {
const requests = await redis.incr(key);
if (requests === 1) {
await redis.expire(key, windowSize);
}
if (requests > limit) {
return res.status(429).json({
error: "Too many requests. Please try again later.",
retryAfter: await redis.ttl(key)
});
}
next();
} catch (err) {
console.error("Redis error:", err);
next(); // Fallback to allow request if Redis is down
}
};
Step 2: Applying to Your Routes
You can apply this middleware globally or to specific sensitive routes like login or search endpoints. Global application is generally safer for public APIs.
const express = require("express");
const app = express();
app.use("/api/", rateLimiter);
app.get("/api/data", (req, res) => {
res.send("Here is your protected data.");
});
app.listen(3000, () => {
console.log("Server running on port 3000");
});
Advanced Considerations
While the example above uses a simple counter, for a true sliding window, you would use a Redis Sorted Set (ZSET). This allows you to store timestamps for every request and count only those within the last 60 seconds using ZREMRANGEBYSCORE. This prevents the "burst" problem associated with fixed windows.
Additionally, always ensure your Redis instance is secured and consider using a fallback mechanism. If your Redis instance goes down, your middleware should ideally allow traffic to pass (fail-open) to avoid a total system outage, while logging an alert for your DevOps team.