Rate Limiting
Objectives
By the end of this lesson, you should be able to:
- Install and configure
express-rate-limiton a login endpoint - Explain what
windowMsandmaxcontrol - Confirm a brute-force attack is actually blocked by rate limiting
💡 Why this matters: The last lesson showed an unprotected login endpoint cracked in 5 requests. This lesson adds the one piece that stops it, without changing the login logic itself at all.
⚠️ A note on verification: every snippet and every output in this lesson was actually run.
Installing express-rate-limit
npm install express-rate-limit
Configuring a Limiter
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 3,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'TooManyRequests', message: 'Too many login attempts, try again later' }
});
windowMs is the size of the time window, here 15 minutes, max is how many requests a single client can make within that window before being blocked, standardHeaders: true adds RateLimit-* response headers so a client can see its own remaining quota, legacyHeaders: false skips the older, deprecated X-RateLimit-* header style. Once a client exceeds max within the window, every further request gets the configured message, with a 429 Too Many Requests status, until the window resets.
Applying It to the Login Route
app.post('/api/v1/auth/login', loginLimiter, async (req, res) => {
const { email, password } = req.body;
const user = users.find(u => u.email === email);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ error: 'InvalidCredentials', message: 'Incorrect email or password' });
}
res.json({ success: true });
});
One line added, loginLimiter as middleware before the route handler, exactly the same pattern as requireAuth or validate(schema) from earlier modules. The login logic itself, hashing, comparison, response shape, is completely unchanged.
Re-Running the Attack
The exact same scripted attack from the last lesson, five passwords, including the correct one last, against this rate-limited endpoint:
Attempt 1: "123456" -> 401 {"error":"InvalidCredentials","message":"Incorrect email or password"}
Attempt 2: "password" -> 401 {"error":"InvalidCredentials","message":"Incorrect email or password"}
Attempt 3: "qwerty" -> 401 {"error":"InvalidCredentials","message":"Incorrect email or password"}
Attempt 4: "letmein" -> 429 {"error":"TooManyRequests","message":"Too many login attempts, try again later"}
Attempt 5: "correct-horse-battery-staple" -> 429 {"error":"TooManyRequests","message":"Too many login attempts, try again later"}
With max: 3, the first three attempts are checked normally, all fail, correctly. The fourth attempt is blocked before it ever reaches the bcrypt comparison, and so is the fifth, the attempt with the actual correct password. The attack that cracked the unprotected endpoint in 5 requests never succeeds at all here, the correct password is never even checked.
Choosing max and windowMs
There’s a real trade-off here: a small max and long windowMs stop attacks hard but can also lock out a legitimate user who mistypes their password a few times in a row, a larger max is friendlier to genuine mistakes but gives an attacker more attempts before being blocked. A common starting point is somewhere around 5 attempts per 15 minutes for a login endpoint specifically, tuned based on real usage patterns, this lesson used max: 3 purely to keep the demonstration short.
Rate Limiting Isn’t Just for Login
Any endpoint that’s expensive, sensitive, or a plausible attack target benefits from a limiter, password reset requests, account creation, and any public API endpoint that costs real server resources per request are all common candidates, each can have its own rateLimit() instance with its own windowMs and max, tuned for that specific endpoint’s needs.
Try It
- Build the rate-limited login route above, and confirm the same attack sequence from the last lesson now gets blocked.
- Change
maxto 10, rerun the attack, and confirm the correct password is found on attempt 5, explain why raisingmaxreintroduces the vulnerability from the last lesson. - Add a separate limiter, with its own
windowMsandmax, to the registration endpoint from Module 1, and explain why registration might reasonably need a different configuration than login. - Inspect the
RateLimit-*response headers on a request that hasn’t hit the limit yet, and explain what each one tells a client.
Recap
express-rate-limitblocks a client aftermaxrequests withinwindowMs, returning429 Too Many Requestsfor the rest of the window.- Adding it to a route is one line of middleware, no change to the route’s own logic.
- Against the brute-force attack from the last lesson, the rate limiter blocks the attack before the correct password is ever reached, real, measured behavior, not a theoretical claim.
Next lesson: managing secrets correctly, and how rate limiting fits into a broader defense-in-depth strategy.