Rate Limiting in Express — Complete Implementation Guide
In this tutorial, you will learn about Rate Limiting in Express. We cover key concepts, practical examples, and best practices to help you master this topic.
Rate limiting in Express is implemented using the express-rate-limit middleware, which provides configurable window-based rate limiting with support for custom stores and per-route configuration.
What You'll Learn
By the end of this tutorial, you will configure express-rate-limit for different use cases, use custom stores for distributed deployments, and handle rate limit violations gracefully.
Why It Matters
Express is the most popular Node.js framework, and express-rate-limit is its standard rate limiting middleware. DodaTech uses it in every Express-based API.
Real-World Use
DodaZIP's Express API uses express-rate-limit with a Redis store to enforce per-user rate limits across multiple server instances in a Kubernetes cluster.
Express Rate Limiting Learning Path
flowchart LR
A[Sliding Window] --> B[Express Rate Limiting]
B --> C[express-rate-limit]
C --> D[Redis Store]
B --> E{You Are Here}
style E fill:#f90,color:#fff
Basic Configuration
Install and configure express-rate-limit with default options. This creates a fixed window rate limiter using in-memory storage.
npm install express-rate-limit
const rateLimit = require("express-rate-limit");
const express = require("express");
const app = express();
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: {
error: "Too many requests, please try again later."
},
standardHeaders: true,
legacyHeaders: false
});
app.use(limiter);
app.get("/", (req, res) => {
res.json({ message: "Welcome" });
});
app.listen(3000);
Per-Route Configuration
Different routes need different rate limits. Use route-specific limiter instances for fine-grained control.
const express = require("express");
const rateLimit = require("express-rate-limit");
const app = express();
const globalLimiter = rateLimit({
windowMs: 60000,
max: 60,
message: { error: "Global limit exceeded" }
});
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true,
message: { error: "Too many login attempts" }
});
const apiLimiter = rateLimit({
windowMs: 60000,
max: 30,
keyGenerator: (req) => req.user?.id || req.ip
});
app.use(globalLimiter);
app.post("/login", authLimiter, (req, res) => {
res.json({ token: "auth-token" });
});
app.get("/api/data", apiLimiter, (req, res) => {
res.json({ data: "api response" });
});
app.listen(3000);
Custom Store with Redis
In-memory storage does not work across multiple server instances. Use the rate-limit-redis package for distributed rate limiting.
npm install rate-limit-redis ioredis
const rateLimit = require("express-rate-limit");
const RedisStore = require("rate-limit-redis");
const Redis = require("ioredis");
const express = require("express");
const app = express();
const redis = new Redis({
host: process.env.REDIS_HOST || "localhost",
port: process.env.REDIS_PORT || 6379
});
const limiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redis.call(...args)
}),
windowMs: 60000,
max: 100,
message: { error: "Rate limit exceeded" }
});
app.use(limiter);
app.get("/", (req, res) => res.json({ ok: true }));
app.listen(3000);
Skipping Rate Limiting
Some requests should not count toward rate limits, such as health checks or internal service calls.
const limiter = rateLimit({
windowMs: 60000,
max: 100,
skip: (req) => {
if (req.path === "/health") return true;
if (req.headers["x-internal-request"]) return true;
return false;
},
keyGenerator: (req) => {
if (req.user?.id) return `user:${req.user.id}`;
if (req.headers["x-api-key"]) return `apikey:${req.headers["x-api-key"]}`;
return `ip:${req.ip}`;
}
});
Common Mistakes
Not setting trust proxy -- Behind a reverse proxy,
req.ipis the proxy's IP. Setapp.set("trust proxy", true).Using default memory store in production -- Memory does not scale across instances. Use Redis or another shared store.
Not configuring skipSuccessfulRequests -- Failed login attempts should count. Successful logins should not count to prevent lockout.
Setting max too low or too high -- Too low blocks legitimate users. Too high does not prevent abuse. Base limits on traffic analysis.
Not returning rate limit headers -- Standard headers help clients self-regulate. Enable them with
standardHeaders: true.
Practice Questions
How do you configure different rate limits for authenticated vs anonymous users? Use a custom
keyGeneratorthat distinguishes between user IDs and IP addresses, with different limiter instances.What happens when the Redis store is unavailable? express-rate-limit throws an error. Implement a fallback memory store or use a circuit breaker.
How do you whitelist specific IPs from rate limiting? Use the
skipoption to bypass rate limiting for whitelisted IPs or internal network ranges.Challenge: Configure rate limiting that increases limits during off-peak hours.
const limiter = rateLimit({
windowMs: 60000,
max: (req) => {
const hour = new Date().getHours();
return hour >= 2 && hour <= 6 ? 200 : 100;
}
});
FAQ
Mini Project
Build a complete rate limiting setup with global limits, per-route limits, Redis store, skip conditions for health checks, and rate limit headers.
const express = require("express");
const rateLimit = require("express-rate-limit");
const RedisStore = require("rate-limit-redis");
const Redis = require("ioredis");
const app = express();
app.set("trust proxy", 1);
const redis = new Redis(process.env.REDIS_URL);
const createLimiter = (opts) => rateLimit({
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
standardHeaders: true,
legacyHeaders: false,
...opts
});
const global = createLimiter({ windowMs: 60000, max: 60 });
const auth = createLimiter({ windowMs: 900000, max: 5, skipSuccessfulRequests: true });
const api = createLimiter({ windowMs: 60000, max: 30, keyGenerator: (req) => req.user?.id || req.ip });
app.use(global);
app.post("/auth/login", auth);
app.use("/api", api);
app.get("/health", (req, res) => res.json({ status: "ok" }));
app.use((err, req, res, next) => {
if (err.code === "LIMIT_UNEXPECTED_EOF") {
return res.status(429).json({ error: "Rate limit store error" });
}
next(err);
});
app.listen(3000);
What's Next
Now that you understand rate limiting in Express, explore using Redis for production rate limiting. Then learn about rate limiting across multiple servers.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro