Credential Stuffing Defence — Rate Limits, MFA, Checks
Credential stuffing replays breached passwords against your login.
20+ years shipping production backend systems. Notes here come from systems that actually shipped.
- ✓A login flow you operate or are building
- ✓Access to authentication logs or analytics
- ✓Basic familiarity with MFA options and password hashing
- Credential stuffing replays username-password pairs from other sites' breaches, succeeding wherever users reused passwords
- Rate-limit login attempts per account and per IP, but prefer progressive delays over hard lockouts that attackers abuse
- Require MFA — ideally WebAuthn or passkeys — so a correct password alone never grants access
- Screen new passwords against breach corpora with the k-anonymity API pattern, never sending full hashes anywhere
- Alert on stuffing waves (spiky failures, password-spray usernames) and rehearse the playbook before the wave hits
Imagine thieves holding a ring of thousands of copied house keys, trying each one in your front door. They didn't pick your lock — they collected keys from doors owners lost, betting you used the same key as your neighbor. That's credential stuffing: attackers replay passwords leaked from other websites against yours. The defense isn't a stronger lock alone — it's limiting how fast anyone can try keys, demanding second proof of identity, and refusing keys already known copied.
Your login page works exactly as designed: it accepts correct passwords and rejects wrong ones. That correctness is the vulnerability. Attackers arrive holding millions of username-password pairs spilled from breaches elsewhere, and your login can't tell a legitimate user from a thief holding that user's reused password. Every attempt looks like an ordinary login, because cryptographically it is one.
Password reuse makes this devastating. People reuse passwords across dozens of sites, so a breach of a forum in 2019 becomes the key to bank, email, and shopping accounts in 2026. Automated tooling tries thousands of pairs per minute, distributed across botnets that dodge naive IP bans, and even a 0.1% success rate on a million-pair list yields a thousand compromised accounts.
Defending logins therefore means changing what 'correct' requires. Rate limiting and bot detection raise the cost of guessing, breach-corpus screening stops users choosing already-stolen passwords, and MFA — especially phishing-resistant WebAuthn — makes a stolen password insufficient by itself. This guide builds that layered defence: what stuffing looks like in your logs, which brakes actually work, and how to respond when the wave arrives.
Why Stolen Passwords Keep Working
Password reuse is the fuel and breach compilations are the match. The average person reuses a handful of passwords across dozens of accounts, and every breach — forums, games, shops, newsletters — adds millions of working pairs to attacker dictionaries. Compilations with billions of entries circulate permanently; a password spilled in 2019 is still being tried against new sites today, because nobody told the password it expired.
Stuffing tooling industrializes the trying. Credential lists feed automated runners that distribute attempts across residential proxies and botnets, solve basic CAPTCHAs, mimic real browsers, and rotate user agents — each IP staying under naive rate limits while the aggregate hammers your endpoint. Success rates look small (fractions of a percent) until you multiply by list sizes in the millions. Your login page's politeness — clear error messages, fast responses, no account enumeration care — helps legitimate users and attackers equally.
Accept the uncomfortable premise: some of your users' passwords are already public, and your login cannot distinguish those users from the thieves holding their credentials. Every defence in this guide follows from that premise. You slow the trying so campaigns cost more than they earn, you reject known-stolen secrets at the door, and you demand proof beyond the password so a correct guess still isn't enough.
Rate Limiting vs Account Lockout: Picking the Right Brake
Not all brakes are safe to press. Hard account lockouts (N failures freeze the account) convert password guessing into account denial: attackers spray bad passwords across your user base and your own security control locks out thousands of legitimate users by morning. Unless paired with instant self-serve unlock (email link, verified device), lockouts are a weapon you hand to the attacker — most teams should avoid them as the primary control.
Progressive delays scale pain with suspicion instead: the first failures cost nothing, then each subsequent failure adds waiting time per account. Humans fat-fingering twice never notice; automation burning thousands of attempts bleeds hours. Layer per-IP buckets underneath for volumetric floods, device fingerprinting to separate known devices from new ones, and step-up challenges (CAPTCHA, then MFA) when velocity or anomaly scores cross thresholds. Each layer asks more of strangers while asking nothing extra of recognized users.
The snippet below shows the per-account progressive delay pattern — the single highest-value control for teams with only IP limiting today. Store attempt counters server-side with expiries, compute delay from consecutive failures, and enforce it before checking the password so timing reveals nothing. Pair it with alerting on aggregate failure ratios, because the delay slows attackers but it's your monitoring that ends campaigns.
MFA and WebAuthn That Survive Stolen Passwords
MFA is the control that makes stuffing mathematically pointless: the breach gave attackers passwords, not second factors. Authenticator apps (TOTP) and hardware keys stop virtually all replayed-password logins for enrolled accounts — the incident above would have been 4,100 failed attempts instead of 4,100 takeovers if those accounts had any second factor at all. The technology works; enrollment is the entire battle.
Not all second factors are equal. SMS codes are better than nothing but vulnerable to SIM swapping and interception; authenticator apps resist those but not real-time phishing (a fake login page relays the code in seconds). WebAuthn and passkeys bind the credential to your site's origin — the key simply won't answer a lookalike domain — defeating phishing and stuffing simultaneously. Prefer platform passkeys for consumers (zero hardware to ship) and hardware keys for admins and finance roles.
Drive enrollment like a product launch, not a suggestion. Require MFA for privileged actions from day one, prompt standard users at natural moments (first login, security settings visits), and celebrate progress with a visible enrollment KPI. For accounts that remain unenrolled, compensate with tighter anomaly thresholds and faster step-up challenges — and keep recovery flows strict, because attackers who can't stuff the front door will try the 'I lost my phone' window next.
Breach Monitoring with the K-Anonymity Pattern
If a user's new password already appears in breach compilations, attackers own it on day one — rejecting it at creation is pure prevention. The k-anonymity API pattern (pioneered by the haveibeenpwned password service) makes this check privacy-safe: hash the candidate password with SHA-1 locally, transmit only the first 5 hex characters, receive all matching suffixes, and compare locally. The service never sees the full hash, let alone the password, yet you learn exactly whether it's been seen in breaches and how often.
Apply the check at every password-set moment: registration, password change, and admin resets — plus periodic re-screening of existing hashes against fresh breach data, since today's safe choice becomes tomorrow's compilation entry. On a match, reject with actionable guidance: explain the password appeared in breaches, suggest a password manager, and offer generated alternatives. Never shame the user; the reuse habit is universal and the fix should feel helpful, not punitive.
The snippet below implements the client side of the pattern — note what's absent: no password leaves the machine, only a 5-character prefix. Cache responses briefly to survive API outages without blocking registration, and fail open carefully (log, allow, and re-check later) rather than failing closed on users during an outage. This single check would have forced resets for the 22,000 additional reused passwords found after the incident above — before attackers tried them.
Bot Signals and Step-up Challenges
Distributed stuffing looks human per-request and inhuman in aggregate — your detection must think in aggregates. Track failure ratios per hour (a jump from 5% to 30%+ is the classic wave signature), unique IPs per targeted account, impossible-travel logins, and datacenter or hosting-ASN origins for supposedly consumer logins. Device fingerprints and behavioral signals (typing cadence, mouse movement, TLS fingerprints) add per-request suspicion scores that feed step-up decisions.
Step-up challenges convert suspicion into cost without punishing real users. Low scores pass silently; medium scores get a CAPTCHA or email verification; high scores demand MFA or block pending review. The key property is asymmetry: each challenge costs automation real money (CAPTCHA solving, phone numbers, time) while costing recognized users nothing. Tune thresholds from logged outcomes — every false challenge teaches you about a legitimate pattern (travel, new phone, password manager autofill).
Keep a human in the loop for tuning, not for triage. Automated playbooks should freeze redemptions, force challenges, and page on-call when wave signatures fire; analysts then review aggregate dashboards, not individual logins. The bash probe below gives responders a one-command wave check: failure ratio plus IP spread over the last hour, the two numbers that distinguish stuffing from a bad deploy or a Monday morning.
Alerting and Playbooks for Stuffing Waves
Waves arrive at 2 AM on holidays — your response quality equals your preparation, not your heroics. Define wave signatures as alerts, not observations: failure-ratio thresholds, IP-spread thresholds, impossible-travel volumes, and fraud-action spikes each page with runbook links attached. The runbook's first page is containment toggles (redemption freeze, global step-up, ASN blocks), because decision fatigue at 3 AM is how 9-hour incidents happen.
Rehearse the playbook quarterly with tabletop exercises: a simulated wave, a communications drill (who tells users, what the status page says, when regulators get notified), and a fraud-account recovery flow that returning victims can complete without calling support. Measure recovery experience like a product metric — victims who reset easily stay customers; victims who fight your process for a week leave and tell everyone.
After each wave, convert findings into permanent controls: new ASN blocks, tightened thresholds, expanded breach-screening, enrollment pushes for victim cohorts. The incident above ended with 22,000 preventive resets that should have been 22,000 rejections at password-set time. Every wave is a free penetration test of your login defences — grade it, fix the gaps it found, and make the next identical campaign fail silently at your front door.
900,000 Stuffing Attempts Took 4,100 Accounts in 9 Hours
- Per-IP limits alone can't see distributed attacks. Throttle per account, fingerprint devices, and challenge anomalies — never rely on IP counting.
- Optional MFA protects almost nobody: 3% enrollment means the control exists on paper while 97% of accounts fall to reused passwords.
- Screen passwords against breach data at every set-or-change moment. A password public for years should never be accepted as new.
| File | Command / Code | Purpose |
|---|---|---|
| auth | from functools import wraps | Rate Limiting vs Account Lockout |
| auth | def password_is_breached(candidate: str, timeout: int = 5) -> bool: | Breach Monitoring with the K-Anonymity Pattern |
| awk '$1 > "$(date -u -d "1 hour ago" +%FT%T)"' /var/log/auth.log \ | Bot Signals and Step-up Challenges | |
| monitoring | groups: | Alerting and Playbooks for Stuffing Waves |
Key takeaways
Common mistakes to avoid
5 patternsRelying on per-IP rate limits against distributed botnets
Using hard account lockouts as the primary brake
Leaving MFA optional and unenrolled
Sending full passwords or hashes to breach-check APIs
Showing distinct errors for unknown users versus wrong passwords
Interview Questions on This Topic
How does credential stuffing differ from brute force and password spraying?
Frequently Asked Questions
20+ years shipping production backend systems. Notes here come from systems that actually shipped.
That's Auth. Mark it forged?
5 min read · try the examples if you haven't