Express.js, a popular Node.js web application framework, doesn’t have built-in rate limiting, leaving developers to implement it themselves or rely on external services.
Here’s how you can implement robust rate limiting for your Express API using the express-rate-limit middleware.
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
// Apply rate limiting to all requests
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per `windowMs`
message: 'Too many requests from this IP, please try again after 15 minutes',
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
app.use(apiLimiter);
// Define your API routes
app.get('/api/users', (req, res) => {
res.json({ message: 'List of users' });
});
app.post('/api/products', (req, res) => {
res.json({ message: 'Product created' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
This setup applies a general rate limit to all incoming requests. The windowMs of 15 * 60 * 1000 (15 minutes) defines the time frame, and max: 100 restricts each IP address to 100 requests within that window. If the limit is exceeded, clients receive a 429 Too Many Requests status code with the specified message.
You can also apply different rate limits to specific routes. For example, to protect a sensitive endpoint:
const sensitiveLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // Limit each IP to 5 requests per `windowMs`
message: 'Too many attempts from this IP, please try again after an hour',
});
app.get('/api/sensitive-data', sensitiveLimiter, (req, res) => {
res.json({ data: 'This is sensitive data' });
});
Here, the /api/sensitive-data route is protected by a stricter limit: only 5 requests per hour per IP.
express-rate-limit uses an in-memory store by default, which is suitable for single-process applications. For multi-process or clustered Node.js environments, you’ll need a shared store like Redis.
To use Redis, install rate-limit-redis:
npm install express-rate-limit express-rate-limit-redis redis
Then, configure it:
const redis = require('redis');
const { createClient } = redis;
const RedisStore = require('rate-limit-redis');
// ... (previous imports and app setup)
const redisClient = createClient({
url: 'redis://localhost:6379' // Replace with your Redis connection URL
});
redisClient.connect().catch(console.error);
const redisStore = new RedisStore({
sendCommand: (...args) => redisClient.sendCommand(args),
});
const apiLimiterWithRedis = rateLimit({
store: redisStore,
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per `windowMs`
message: 'Too many requests from this IP, please try again after 15 minutes',
});
app.use(apiLimiterWithRedis);
// ... (rest of your app)
When using a distributed store like Redis, express-rate-limit coordinates the rate limiting across all instances of your application, ensuring consistent enforcement.
The middleware generates RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset headers in the response. These are crucial for clients to understand their current rate limit status. RateLimit-Reset indicates the time (in UTC epoch seconds) when the count will reset.
The keyGenerator option allows you to customize how the rate limit key is generated. By default, it uses req.ip. You can modify this to use, for example, a user ID from the session or a JWT token for authenticated users:
const customKeyGenerator = (req, res) => {
// Example: Use user ID if available, otherwise fallback to IP
if (req.user && req.user.id) {
return `user_${req.user.id}`;
}
return req.ip;
};
const userSpecificLimiter = rateLimit({
keyGenerator: customKeyGenerator,
windowMs: 60 * 60 * 1000, // 1 hour
max: 1000,
message: 'Too many requests, please try again later',
});
// Assuming you have authentication middleware that populates req.user
// app.use('/api/authenticated', authMiddleware, userSpecificLimiter, (req, res) => { ... });
This allows for granular control, preventing a single authenticated user from overwhelming the system, irrespective of their IP address.
If you need to skip rate limiting for specific IP addresses (e.g., internal health check services), use the skip option:
const alwaysAllowLocalhost = (req, res) => {
return req.ip === '127.0.0.1' || req.ip === '::ffff:127.0.0.1';
};
const apiLimiterWithSkip = rateLimit({
skip: alwaysAllowLocalhost,
windowMs: 15 * 60 * 1000,
max: 100,
message: 'Too many requests from this IP, please try again after 15 minutes',
});
app.use(apiLimiterWithSkip);
This ensures that requests originating from localhost bypass the rate limiting entirely.
The handler option lets you define a custom function to execute when the rate limit is exceeded. This is useful for more advanced logging or error handling:
const customErrorHandler = (req, res, options) => {
console.warn(`Rate limit exceeded for IP: ${req.ip} at ${new Date()}`);
res.status(options.statusCode).send(options.message);
};
const apiLimiterWithHandler = rateLimit({
handler: customErrorHandler,
windowMs: 15 * 60 * 1000,
max: 100,
message: 'Rate limit exceeded. Please wait.',
});
app.use(apiLimiterWithHandler);
This example logs a warning to the console when a limit is hit and then sends the custom response.
The draft_poller_request header is a common pattern for clients to signal they are performing automated polling or scraping and might exceed typical user request rates. express-rate-limit respects this by allowing you to configure a specific limit for requests bearing this header. For instance, you could set a higher limit for these requests or even a different window.
const pollerLimiter = rateLimit({
windowMs: 30 * 60 * 1000, // 30 minutes
max: 500, // Allow 500 requests if they are marked as pollers
message: 'Too many polling requests from this IP, please try again later',
keyGenerator: (req, res) => req.ip + '_poller', // Differentiate poller keys
handler: (req, res, options) => {
console.log(`Poller rate limit exceeded for IP: ${req.ip}`);
res.status(options.statusCode).send(options.message);
}
});
app.use((req, res, next) => {
if (req.headers['draft_poller_request']) {
pollerLimiter(req, res, next);
} else {
next();
}
});
This approach allows you to differentiate between regular user traffic and automated requests, providing more nuanced control over your API’s resource consumption.