Credential stuffing is an automated attack that replays username and password pairs stolen from other websites' data breaches against your login page, betting that people reuse passwords. Because each pair is tried only once or twice, it slips past simple per-account limits — so the defence is layered: per-IP rate limiting that returns HTTP 429, bot detection at the edge, and multi-factor authentication as the backstop. This guide explains the attack and how to defend against it.
For the site owner (plain English)
When a big company gets breached, the leaked email-and-password lists end up for sale. Attackers know most people reuse the same password everywhere, so they take those lists and try them, automatically, against thousands of other sites — including yours. They are not guessing; they already have real passwords, and they are just checking which ones still work. This is credential stuffing, and it is one of the most common ways ordinary accounts get taken over. You cannot stop other companies getting breached, but you can make their leaks useless against your site: limit how fast anyone can attempt logins, challenge suspicious traffic, and turn on multi-factor authentication so a correct password alone is not enough to get in.
How credential stuffing works
The attacker feeds a tool a list of leaked email:password pairs, spreads the requests across many IP addresses (often a botnet or proxy pool), and submits each pair to your login endpoint. A small percentage succeed — the users who reused their breached password — and those accounts are then drained, resold, or used for fraud. The attack is high-volume but low per-account: each username might be tried only once, which is exactly why it evades a limit that only counts failures against a single account.
This is the mirror image of brute force, which throws many passwords at one account — see how to stop brute-force attacks. Both are automated password attacks, and both are addressed by the umbrella practice of rate-limiting your login page.
The layered defence
- Per-IP rate limiting. Because per-account limits barely register a stuffing run, cap login attempts per IP over a short window and return 429 with
Retry-Afterwhen exceeded. - Multi-factor authentication. The single strongest control: a correct stolen password cannot complete a login without the second factor.
- Bot detection / CAPTCHA. Edge platforms and challenges (Turnstile, hCaptcha) distinguish automated tools from real browsers and block the volume.
- Breached-password screening. Reject passwords known to appear in breach corpora at sign-up and reset (the Have I Been Pwned Pwned Passwords range API supports this without sending the password).
- Anomaly monitoring. A sudden spike in login volume, failure rate, or new geographies is the signature of a stuffing run — alert on it.
This aligns with the OWASP Credential Stuffing Prevention Cheat Sheet, the primary reference for this attack.
Developer: per-IP limiting and screening
A per-IP throttle on the auth route is the minimum. Here it is in the common stacks; each returns 429 when the limit trips.
Node / Express — express-rate-limit
import rateLimit from "express-rate-limit";
const authLimiter = rateLimit({
windowMs: 10 * 60 * 1000,
limit: 30, // per IP — catches high-volume stuffing
standardHeaders: true, // sends Retry-After
legacyHeaders: false,
});
app.post("/api/auth/login", authLimiter, loginHandler);Django — django-axes (per IP)
# settings.py
AXES_FAILURE_LIMIT = 20
AXES_COOLOFF_TIME = 1 # hours
AXES_LOCKOUT_PARAMETERS = ["ip_address"] # per-IP catches the stuffing volumeRuby / Rack::Attack (per IP)
# config/initializers/rack_attack.rb
Rack::Attack.throttle("auth/ip", limit: 30, period: 10.minutes) do |req|
req.ip if req.path == "/api/auth/login" && req.post?
endJava / Spring Security (per-IP block)
// Track attempts keyed by client IP, then in a filter:
if (attemptService.isBlocked(clientIp)) {
response.setStatus(429);
response.setHeader("Retry-After", "60");
return;
}Raw HTTP — the response
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{"error": "too_many_requests", "retry_after": 60}For the meaning of that status line and header, see the HTTP 429 Too Many Requests guide.
See whether your login endpoint limits high-volume requests — free, no signup.
Run the login rate limit checker →Read this alongside the pillar, how to rate-limit your login page, and the brute-force companion, how to stop brute-force attacks.