Rate Limiting & Abuse Control
Tiered limits, a shared store for multi-instance deployments, and trust proxy so you throttle the real client, not your load balancer.
A single global limiter is a blunt instrument. Real protection is tiered: a generous ceiling on the whole API, a strict one on /auth/login and /auth/forgot-password, and perhaps a per-user quota on expensive endpoints.
The multi-instance trap
express-rate-limit's default store is in-memory. Run three instances behind a load balancer and you have three independent counters — effectively triple your intended limit, and the count resets on every deploy. In production, back it with a shared store like Redis.
Get the client IP right
Behind a proxy, every request appears to come from the proxy's IP, so one bad actor would rate-limit everyone. You must set app.set('trust proxy', 1) so Express reads the real IP from X-Forwarded-For.
app.set('trust proxy', 1); // trust exactly one hop (your LB)Example
import express from 'express';
import { rateLimit } from 'express-rate-limit';
// import { RedisStore } from 'rate-limit-redis'; // for multi-instance
const app = express();
// One hop: our load balancer. Never use `true` in production.
app.set('trust proxy', 1);
// Generous ceiling for the whole API.
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 1000,
standardHeaders: 'draft-7', // RateLimit-* headers
legacyHeaders: false,
// store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
});
// Strict limiter for credential endpoints — slows brute force to a crawl.
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 5,
skipSuccessfulRequests: true, // only failed attempts count toward the limit
message: { error: 'Too many attempts, try again later.' },
});
app.use('/api', apiLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/forgot-password', authLimiter);When to use it
- A multi-tier API applies a global 1000 req/hr rate limit, a stricter 5 req/15min limit on the login endpoint, and a 10 req/min limit on the password-reset endpoint.
- A team stores rate limit counters in Redis so the limit is enforced consistently when the Express app runs in a multi-process cluster.
- A developer implements a sliding window rate limiter to prevent clients from bursting 100 requests in the last second of each window.
More examples
Layered rate limits
Applies a lenient global limit to all API routes and a strict limit on the login endpoint to defend against brute-force.
const rateLimit = require('express-rate-limit');
const globalLimiter = rateLimit({ windowMs: 60 * 60 * 1000, max: 1000 });
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5,
message: { error: 'Too many login attempts' } });
app.use('/api', globalLimiter);
app.post('/api/auth/login', authLimiter);Redis-backed rate limiter
Uses rate-limit-redis to store counters in Redis so multiple Express processes share the same rate limit state.
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const { createClient } = require('redis');
const client = createClient({ url: process.env.REDIS_URL });
client.connect();
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
store: new RedisStore({ sendCommand: (...args) => client.sendCommand(args) }),
});
app.use('/api', limiter);IP + user key rate limiting
Uses a custom keyGenerator to rate-limit authenticated users by their ID and unauthenticated requests by IP.
const rateLimit = require('express-rate-limit');
const perUserLimiter = rateLimit({
windowMs: 60 * 1000,
max: 60,
keyGenerator: (req) => req.user?.id || req.ip, // authenticated users by ID
});
app.use('/api', perUserLimiter);
Discussion