Express Rate Limit Middleware — Node.js API Throttling with express-rate-limit
In this tutorial, you will learn about Express Rate Limit Middleware. We cover key concepts, practical examples, and best practices to help you master this topic.
The express-rate-limit middleware provides simple yet powerful rate limiting for Express.js applications with built-in memory store, custom store support (Redis, MongoDB), and configurable response handling.
What You'll Learn
- How to install and configure express-rate-limit
- How to use Redis as a distributed rate limit store
- How to create custom skip and key generation functions
Why It Matters
Express is one of the most popular Node.js frameworks. Adding rate limiting via middleware is the simplest way to protect your API. Express-rate-limit handles the complexity of window tracking, header generation, and response formatting out of the box.
Real-World Use
DodaTech's Node.js API uses express-rate-limit with three configurations: a global limiter (100 req/min per IP), an auth endpoint limiter (5 req/min per IP using keyGenerator), and a per-user limiter that uses the authenticated user ID from the JWT token.
flowchart LR
A["Request"] --> B["Global Rate\nLimiter"]
B --> C{"Auth\nEndpoint?"}
C -->|"Yes"| D["Auth Rate\nLimiter (5/min)"]
C -->|"No"| E["Standard Rate\nLimiter (100/min)"]
D --> F{"Within\nlimit?"}
E --> F
F -->|"Yes"| G["Route Handler"]
F -->|"No"| H["429 Response"]
style B fill:#dbeafe,stroke:#2563eb
style D fill:#fef3c7,stroke:#d97706
style H fill:#fecaca,stroke:#dc2626
Basic Configuration
const rateLimit = require('express-rate-limit');
const express = require('express');
const app = express();
// Global rate limiter
const globalLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute window
max: 100, // 100 requests per minute
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false, // Disable X-RateLimit-* headers
message: {
error: 'rate_limit_exceeded',
message: 'Too many requests, please try again later.',
retryAfter: '60 seconds'
}
});
app.use('/api/', globalLimiter);
Per-Endpoint Rate Limits
// Strict limiter for auth endpoints
const authLimiter = rateLimit({
windowMs: 60 * 1000,
max: 5,
message: {
error: 'auth_rate_limit',
message: 'Too many authentication attempts.',
retryAfter: 60
},
keyGenerator: (req) => {
// Use IP + username for auth rate limiting
return req.ip + ':' + (req.body.username || '');
},
handler: (req, res) => {
res.status(429).json({
error: 'auth_rate_limit',
message: 'Too many login attempts. Account temporarily locked.',
retryAfter: 60,
lockoutDuration: '5 minutes'
});
}
});
app.use('/api/auth/login', authLimiter);
// Generous limiter for read-only endpoints
const readLimiter = rateLimit({
windowMs: 60 * 1000,
max: 500,
message: {
error: 'rate_limit_exceeded',
message: 'Read limit exceeded.'
}
});
app.use('/api/public/', readLimiter);
Redis Store Configuration
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');
const redisClient = new Redis({
host: 'redis-cluster.dodatech.com',
port: 6379,
enableOfflineQueue: false
});
const distributedLimiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redisClient.call(...args),
prefix: 'rl:express:'
}),
windowMs: 60 * 1000,
max: 100,
standardHeaders: true,
// Skip rate limiting for health checks
skip: (req) => req.path === '/health'
});
app.use(distributedLimiter);
// Health check endpoint (not rate limited)
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
Custom Key Generator
// Rate limit by authenticated user ID
const userRateLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
keyGenerator: (req) => {
// Use JWT user ID if authenticated, fall back to IP
if (req.user && req.user.id) {
return `user:${req.user.id}`;
}
return `ip:${req.ip}`;
},
skipFailedRequests: false,
requestWasSuccessful: (req, res) => {
return res.statusCode < 400;
}
});
Common Mistakes
1. Using Default Memory Store in Production
The default memory store does not scale across multiple server instances. Use Redis or another shared store.
2. Not Setting standardHeaders
Without standard headers, clients cannot determine remaining limits or retry times. Enable standardHeaders for better client experience.
3. Forgetting to Skip Health Checks
Health check endpoints should be excluded from rate limiting to prevent false monitoring alerts.
4. Using the Same Limiter for All Routes
Different endpoints need different limits. Create separate limiters for auth, write, and read endpoints.
5. Not Handling Rate Limit Errors Gracefully
Customize the error response to include helpful information like retry timing and which limit was exceeded.
Practice Questions
- What is the default store for express-rate-limit?
- How do you configure per-endpoint rate limits?
- What is a keyGenerator function used for?
- Why use Redis as a store instead of the default memory store?
- How do you skip rate limiting for specific requests?
Answers
- In-memory store (MemoryStore). 2. Create separate rateLimit instances and apply them to different routes. 3. To customize the key used for rate limit counting (e.g., by user ID). 4. For distributed rate limiting across multiple server instances. 5. Use the
skipoption with a function that returns true for requests to skip.
Challenge
Build an Express.js API with three different rate limiters: global (100 req/min), auth (5 req/min with username-based key), and per-user (1000 req/min based on JWT). Use Redis for distributed storage. Include a test script that verifies each limiter.
FAQ
Mini Project
Create an Express.js API with: a global rate limiter (50 req/min per IP), auth endpoint limiter (3 req/min per username), data endpoint limiter (200 req/min per user ID), Redis-backed distributed store, custom error responses with upgrade suggestions, and a Locust load test script to verify all limits.
What's Next
- Learn about Django ratelimit for Python web applications
- Explore Spring Boot rate limiting with Bucket4j
- Continue to FastAPI rate limiting with SlowAPI
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro