Rate limiting
Express has no built-in rate limiting. express-rate-limit is the usual answer, and
the thing to understand before adding it is that it keys on req.ip — so its
correctness is entirely inherited from trust proxy.
Rated medium deliberately. A rate limit slows credential stuffing and brute force;
it does not close a vulnerability, and it is not a substitute for the account
protections underneath it.
import rateLimit from "express-rate-limit";
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 10, standardHeaders: "draft-8", legacyHeaders: false,});
app.use("/login", authLimiter);app.use("/password-reset", authLimiter);Apply it to the endpoints that matter rather than globally. A site-wide limit that is loose enough not to annoy real users is usually too loose to stop a login attack, and a limit tight enough to stop the attack breaks normal browsing.
for i in $(seq 1 12); do curl -s -o /dev/null -w '%{http_code} ' -X POST https://example.com/login -d 'u=a&p=b'done; echoYou want to see the status flip to 429 before the twelfth request. Run it from an
address that is not allow-listed, and remember that if the flip never comes the
problem may be trust proxy rather than the limiter.
The store, and the thing that surprises people
Section titled “The store, and the thing that surprises people”The default store is in-memory and per process. Run four instances behind a load balancer and the effective limit is four times what you configured; restart a container and every counter resets. For a limit that actually holds, back it with a shared store (Redis, Memcached) the same way you did the session store.
It validates your proxy configuration for you
Section titled “It validates your proxy configuration for you”express-rate-limit ships startup validators, and two of them exist purely because this is the mistake everyone makes. From its source:
ERR_ERL_PERMISSIVE_TRUST_PROXY— “The Express ‘trust proxy’ setting is true, which allows anyone to trivially bypass IP-based rate limiting.”ERR_ERL_UNEXPECTED_X_FORWARDED_FOR— “The ‘X-Forwarded-For’ header is set but the Express ‘trust proxy’ setting is false (default).”
The first means the limiter can be defeated by sending a header. The second means every visitor is being counted as the same client, so the limit will fire for everyone at once and look like an outage.
Both are worth treating as errors rather than notices. The full list of error strings is on the trust proxy page, which is where the fix lives.
A limiter that counts everyone as one client locks out your entire user base the
moment traffic rises — and it will look like a capacity problem, not a configuration
one. Check trust proxy before turning limits down.
Health checks, uptime monitors and your own CI will hit these endpoints too. Exempt them explicitly rather than raising the limit until they stop failing.
Related
Section titled “Related”- trust proxy — this control is downstream of that one
- Rate limiting at the proxy is cheaper under load and fails the same way: a limiter keyed on the proxy’s own view of the client address has the identical blind spot.