Script Valley
Authentication From Scratch
Security HardeningLesson 5.1

How to add rate limiting to login endpoints

express-rate-limit, sliding window, brute force prevention, per-IP limiting, per-username limiting, lockout vs slowdown, 429 status code

Why Login Endpoints Need Rate Limiting

Without rate limiting, an attacker can make thousands of login attempts per second. bcrypt slows individual comparisons, but at scale that protection erodes. Rate limiting stops the attack before password verification even runs.

npm install express-rate-limit
const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 10, // max 10 attempts per IP per window
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many login attempts, try again later' },
  skipSuccessfulRequests: true // only count failures
});

app.post('/auth/login', loginLimiter, loginController);

Per-Username Rate Limiting

IP-based rate limiting fails against distributed attacks (botnet using many IPs). Supplement with per-username limiting: track failed attempts per email address in Redis or a database. After 5 failures on a specific account, require a CAPTCHA or temporary lock.

// Pseudo-code: check per-email failures before verifying password
const failures = await redis.incr(`login_fails:${email}`);
await redis.expire(`login_fails:${email}`, 900); // 15 min TTL
if (failures > 5) return res.status(429).json({ error: 'Account temporarily locked' });

Always return a 429 status with a Retry-After header so clients and browsers handle the response correctly.

Up next

CSRF protection for cookie-based auth

Sign in to track progress