Skip to content

Rate limiting

Severity: mediumApplies to: express-rate-limit 8.xFacts last verified 2026-07-26 against express-rate-limit 8.6.0

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.

the fix
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.

verify it workedRun this in: http response
Terminal window
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; echo

You 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.

before you ship this

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.

  • 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.