Credential stuffing has quietly become the default way attackers break into APIs. Bots armed with billions of leaked username-password pairs no longer bother guessing passwords character by character — they replay credentials stolen from other breaches, betting that people reuse logins across services. According to an authentication threat report published by MojoAuth, defenders blocked 4.2 billion credential stuffing attempts in 2025 alone, a 47% year-over-year jump. If your API sits behind a login form, a mobile app, or a partner integration, it is almost certainly being tested right now. This tutorial walks through building a layered defense in 12 concrete steps, with working code for rate limiting, breach-password screening, passkeys, and anomaly detection, plus a full working project you can adapt to your own stack.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
Why Credential Stuffing Is Surging Through APIs in 2026
Credential stuffing used to be a browser problem — bots hammering a website’s login page. That has shifted. Salt Security’s API attack research found that among production APIs it assessed, 41% had authentication problems and 27% showed active credential stuffing or brute-force issues, alongside 44% with sensitive data exposure and 43% with other vulnerabilities. APIs are now the preferred entry point because they are often less protected than the web frontend sitting in front of them: fewer CAPTCHAs, thinner rate limits, and authentication logic that was written once and rarely revisited.
The attacks themselves have also gotten harder to spot. A 2026 authentication threat landscape report found that 89% of credential stuffing attempts now route through residential proxy networks, which makes simple IP blocking close to useless since the traffic looks like it is coming from ordinary home internet connections. The same research recorded 37 million password spray attempts with a median success rate of just 1.8% — a number that sounds small until you remember attackers are running it against millions of accounts at once, and even a sliver of a percent turns into thousands of compromised logins. Attacks are also lasting longer: median campaign duration climbed from 1.8 hours in 2024 to 3.2 hours in 2025, and Q4 shopping season sees roughly 2.1x the normal volume.
Financial APIs have it worst. A 2026 API security statistics report found credential stuffing attacks against financial APIs rose 45% year-over-year in 2025. The Verizon 2025 Data Breach Investigations Report, analyzing SSO provider logs, found a median of 19% of all authentication attempts reviewed were credential stuffing — not phishing, not brute force, just replayed credentials. That is the baseline you are defending against, and it does not go away because your API has a login screen instead of a web form.
The dollar cost tracks the volume. Industry loss estimates place the average cost of a credential-stuffing-driven breach in the multi-million-dollar range once fraud, remediation, and customer churn are factored in, and that number climbs fast for any business handling payments or stored financial data. The OWASP API Security Top 10 project lists broken authentication as one of the most common and most damaging API risk categories precisely because a single successful credential-stuffing campaign can cascade into account takeover, fraud, and data exfiltration in one motion. None of this is new in concept — credential stuffing has existed for well over a decade — but the scale, the proxy infrastructure behind it, and the fact that it now targets APIs specifically rather than login pages is what has changed heading into the second half of 2026.
What Credential Stuffing Actually Is (and Isn’t)
Credential stuffing is the automated testing of previously breached username-password pairs against a different service, betting on password reuse. It is distinct from brute forcing (which guesses passwords without prior knowledge) and password spraying (which tries one common password against many accounts to dodge lockouts). Confusing the three leads teams to deploy the wrong defense — a strong password policy does nothing against credential stuffing, because the passwords being tried are often already strong; they were simply exposed somewhere else.
| Attack type | How it works | Best primary defense |
|---|---|---|
| Credential stuffing | Replays real, previously breached username-password pairs | Breach-password screening, MFA, device/behavior signals |
| Brute force | Systematically guesses passwords for one account | Rate limiting, account lockout, CAPTCHA |
| Password spraying | Tries a handful of common passwords across many accounts | Lockout thresholds tuned per-account, not just per-IP |
| Account takeover (ATO) | The end result of any of the above succeeding | Post-login anomaly detection, step-up auth |
The practical implication for API teams: you cannot rely on your identity provider’s default settings, because most were designed around browser-based login flows with cookies, referrers, and JavaScript challenges — none of which exist the same way in a pure API context. A mobile app or a partner’s server calling your /auth/login endpoint looks structurally identical whether it is a real user or a credential-stuffing bot, which is exactly why this defense has to be built in layers rather than relying on any single control.
Prerequisites and Tools You’ll Need
You do not need an enterprise security budget to build a credible defense. This tutorial uses widely available, mostly free tooling. Adjust the specific products to whatever your stack already uses — the layered approach matters more than the exact vendor.
- A backend runtime such as Node.js (current LTS release) or an equivalent in your language of choice — the code samples below use Express-style middleware patterns that translate easily to Python, Go, or Java
- Redis 7.x or later, for shared rate-limit counters and token buckets across API instances
- An API gateway or reverse proxy that supports rate limiting and custom plugins — examples here use Kong Gateway and Nginx, but AWS API Gateway, Azure API Management, and Apigee all support equivalent configuration
- Access to the Have I Been Pwned Pwned Passwords API for breach-password screening (free, k-anonymity model, no full password ever leaves your server)
- A WebAuthn/FIDO2 library for passkey support, such as
@simplewebauthn/serverfor Node.js or an equivalent in your framework - Centralized logging (ELK stack, Datadog, Splunk, or your SIEM of choice) for correlating authentication events
- A staging environment that mirrors production auth flows, for safely load-testing your defenses before rollout
- Administrative access to your identity provider (Okta, Entra ID, Auth0, or homegrown) to configure MFA and risk policies
Budget 90 to 120 minutes to work through all 12 steps in a test environment. Production rollout, especially for MFA enforcement and rate-limit tuning, should happen gradually over days or weeks — more on why in the pitfalls section below.
Step 1: Audit and Inventory Every Authentication Endpoint
You cannot defend an endpoint you do not know exists. Start by pulling a complete inventory of every route that accepts credentials: primary login, password reset request, password reset confirmation, MFA verification, MFA resend, token refresh, account recovery, and any legacy or internal-only auth routes that predate your current identity provider. Security teams routinely miss the password-reset and MFA-resend paths because they feel like secondary flows, but a May 2026 API security audit found these are exactly where attackers pivot when the primary login is hardened — unthrottled MFA resend endpoints and password-reset flows that leak whether an email exists are both common, exploitable gaps.
For each endpoint, document: whether it is currently rate-limited, whether it returns different responses for valid versus invalid usernames (an enumeration leak), whether it is reachable without a valid API key or referrer check, and what monitoring currently exists. This audit becomes your prioritization list for the remaining steps — endpoints with the highest traffic and the weakest current controls go first.
Step 2: Build Layered Rate Limiting (Per-IP, Per-User, Per-Endpoint)
Rate limiting is the first real barrier against credential stuffing, but a single global threshold is not enough. A July 2026 API best-practices guide recommends applying limits at three levels simultaneously: per IP address (to slow down any single source), per user account (to stop distributed attacks targeting one victim from many IPs), and per endpoint (because a login route and a password-reset route need very different thresholds). Below is a Redis-backed sliding-window limiter in Node.js that applies all three layers before a request ever reaches your authentication logic.
// rateLimiter.js — layered sliding-window limiter using Redis
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
async function checkLimit(key, maxRequests, windowSeconds) {
const now = Date.now();
const windowStart = now - windowSeconds * 1000;
const multi = redis.multi();
multi.zremrangebyscore(key, 0, windowStart);
multi.zadd(key, now, `${now}-${Math.random()}`);
multi.zcard(key);
multi.expire(key, windowSeconds);
const results = await multi.exec();
const count = results[2][1];
return count <= maxRequests;
}
async function credentialStuffingGuard(req, res, next) {
const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.ip;
const username = (req.body?.username || '').toLowerCase().trim();
const endpoint = req.path;
const ipOk = await checkLimit(`rl:ip:${ip}:${endpoint}`, 20, 60);
const userOk = username
? await checkLimit(`rl:user:${username}`, 5, 60)
: true;
const endpointOk = await checkLimit(`rl:endpoint:${endpoint}`, 500, 10);
if (!ipOk || !userOk || !endpointOk) {
return res.status(429).json({ error: 'rate_limited', retry_after: 60 });
}
next();
}
module.exports = { credentialStuffingGuard };
Tune the numbers to your real traffic. A login endpoint that legitimately sees a single user retry a typo twice needs a per-user threshold around 5-10 attempts per minute; a password-reset request endpoint should be far stricter, closer to 3 per hour per account, since legitimate users rarely trigger it repeatedly. Log every 429 response with the triggering key so you can spot patterns before tightening further.
Step 3: Add Adaptive Challenges Instead of Blanket CAPTCHAs
Slapping a CAPTCHA on every login request kills conversion and still gets bypassed by CAPTCHA-solving services. The better pattern is adaptive: only challenge requests that already show risk signals — a new device, an IP with no prior history against that account, or a request that has already tripped a soft rate-limit threshold. Nginx can enforce a baseline request-shaping layer in front of your application so the adaptive logic only has to deal with traffic that already passed a coarse filter.
# nginx.conf — request-shaping layer in front of the auth service
http {
limit_req_zone $binary_remote_addr zone=login_ip:10m rate=20r/m;
limit_req_zone $http_x_device_id zone=login_device:10m rate=10r/m;
server {
location /api/auth/login {
limit_req zone=login_ip burst=5 nodelay;
limit_req zone=login_device burst=3 nodelay;
limit_req_status 429;
proxy_pass http://auth_backend;
}
location /api/auth/password-reset {
limit_req zone=login_ip burst=2 nodelay;
limit_req_status 429;
proxy_pass http://auth_backend;
}
}
}
Behind this layer, your application code decides whether to issue a proof-of-work challenge (cheap, invisible to real users, expensive for bots running at scale) or a visible CAPTCHA (reserved for the highest-risk requests only). Reserve the visible challenge for cases where the risk score is already elevated — new device plus failed attempt plus no prior session history, for example — rather than showing it to everyone.
Step 4: Deploy Bot and Device Fingerprint Detection
IP-based blocking alone is close to obsolete against credential stuffing, given that 89% of attempts now route through residential proxies according to 2026 threat landscape data. Device and behavioral fingerprinting fills that gap by looking at signals that are harder to rotate than an IP address: TLS handshake characteristics, HTTP header ordering, request timing patterns, and for mobile apps, device attestation tokens (Apple's App Attest or Android's Play Integrity API).
You do not need to build fingerprinting from scratch. Commercial bot-management products (Cloudflare Bot Management, Akamai Bot Manager, DataDome, Arkose Labs) plug in at the CDN or gateway layer and score every request before it reaches your application. If budget is a constraint, an open-source starting point is tracking TLS JA3/JA4 fingerprints combined with request-timing entropy — genuine mobile apps and browsers produce far more timing variance than a scripted bot replaying credentials in a tight loop. Feed the resulting risk score into the same decision point from Step 3: low risk passes through, medium risk gets a proof-of-work challenge, high risk gets blocked or hard-challenged.
Step 5: Screen Passwords Against Breach Databases
Credential stuffing only works because the password being tried was already exposed in a prior breach. Blocking known-breached passwords at signup and password-change time directly cuts the pool of credentials attackers can successfully replay against your service. The Have I Been Pwned Pwned Passwords API uses k-anonymity: you send only the first five characters of a SHA-1 hash, never the password or the full hash, and get back a list of matching suffixes with breach counts.
// checkBreachedPassword.js
const crypto = require('crypto');
const fetch = require('node-fetch');
async function isPasswordBreached(password) {
const sha1 = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
const prefix = sha1.slice(0, 5);
const suffix = sha1.slice(5);
const res = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`, {
headers: { 'Add-Padding': 'true' }
});
const text = await res.text();
const match = text.split('\r\n').find(line => line.startsWith(suffix));
if (!match) return { breached: false, count: 0 };
const count = parseInt(match.split(':')[1], 10);
return { breached: count > 0, count };
}
module.exports = { isPasswordBreached };
Run this check at signup, at password change, and periodically against your existing user base (offline, never live during login, since you should not be hashing and checking passwords on every authentication attempt). NIST's Special Publication 800-63B digital identity guidelines explicitly recommend screening new passwords against known-breach corpora rather than relying solely on complexity rules, since complexity requirements do nothing to stop reuse of an already-strong-but-exposed password. OWASP's own Credential Stuffing Prevention Cheat Sheet makes the same point and adds that breach-password screening should be paired with multi-factor authentication rather than deployed as a standalone fix, since no password-side control catches every case.
Step 6: Roll Out Phishing-Resistant MFA and Passkeys
Every layer above reduces successful credential stuffing attempts, but MFA is what stops a successful password match from becoming an account takeover. A March 2026 guide for startup security teams put it plainly: enforcing phishing-resistant MFA — passkeys or hardware security keys, not SMS codes — eliminates the credential-reuse vector entirely, because a correct password alone no longer grants access. WebAuthn/FIDO2 passkeys are the strongest option available today and, unlike SMS or TOTP, cannot be phished or replayed, since the cryptographic challenge is bound to the origin domain.
// passkeyRegistration.js — using @simplewebauthn/server
const { generateRegistrationOptions, verifyRegistrationResponse } = require('@simplewebauthn/server');
async function startPasskeyRegistration(user) {
const options = await generateRegistrationOptions({
rpName: 'Your API Platform',
rpID: 'yourdomain.com',
userID: Buffer.from(user.id),
userName: user.email,
attestationType: 'none',
authenticatorSelection: {
residentKey: 'required',
userVerification: 'preferred',
},
});
await saveChallenge(user.id, options.challenge);
return options;
}
async function finishPasskeyRegistration(user, response) {
const expectedChallenge = await getChallenge(user.id);
const verification = await verifyRegistrationResponse({
response,
expectedChallenge,
expectedOrigin: 'https://yourdomain.com',
expectedRPID: 'yourdomain.com',
});
if (verification.verified) {
await savePasskeyCredential(user.id, verification.registrationInfo);
}
return verification.verified;
}
Roll passkeys out as an optional, encouraged upgrade first, then move toward requiring phishing-resistant MFA for high-value actions (payments, admin access, API key generation) even for accounts still using passwords. This gives you a meaningful security uplift immediately without forcing every user through a migration on day one.
Step 7: Build Behavioral Anomaly Detection
Credential stuffing that gets past rate limiting and bot detection still leaves a trail in post-login behavior. Security researchers writing about API-focused credential stuffing in August 2026 recommended correlating sign-in behavior with downstream actions — profile edits, payment attempts, unusual API parameter changes — rather than treating authentication as a one-time gate. A successful stuffing attack often shows up as a login from an unfamiliar location immediately followed by rapid, scripted API calls that a real human would not make at that speed.
// Example detection query, Elasticsearch DSL style
{
"query": {
"bool": {
"must": [
{ "range": { "@timestamp": { "gte": "now-15m" } } },
{ "term": { "event.action": "login_success" } }
]
}
},
"aggs": {
"by_account": {
"terms": { "field": "user.email", "min_doc_count": 1 },
"aggs": {
"distinct_ips": { "cardinality": { "field": "source.ip" } },
"distinct_countries": { "cardinality": { "field": "geo.country_iso_code" } },
"flag_impossible_travel": {
"bucket_selector": {
"buckets_path": { "ips": "distinct_ips", "countries": "distinct_countries" },
"script": "params.ips > 2 || params.countries > 1"
}
}
}
}
}
}
This query flags accounts with successful logins from multiple countries or more than two distinct IPs within a 15-minute window — a strong signal of either impossible travel or a compromised credential being used from a botnet. Route flagged accounts into a step-up authentication challenge or a temporary hold rather than an automatic ban, since false positives (a user on a VPN, for instance) are common enough that hard blocking creates support tickets faster than it stops attackers.
Step 8: Harden the API Gateway and WAF Layer
Your API gateway is the natural place to enforce many of the controls above without touching application code. An August 2026 API hardening guide recommends a specific baseline: JWTs signed with RS256 or ES256 with the algorithm pinned server-side (never trust an alg value from the token itself), token expiry of 60 minutes or less, strict schema validation on every input, mass-assignment prevention through explicit field allow-lists, TLS 1.2 or higher only, and disabling GraphQL introspection in production if you run a GraphQL layer. If you use Kong Gateway, the rate-limiting and bot-detection plugins can enforce most of this declaratively.
# kong.yml — declarative config for the auth route
_format_version: "3.0"
services:
- name: auth-service
url: http://auth-backend:3000
routes:
- name: login-route
paths: ["/api/auth/login"]
methods: ["POST"]
plugins:
- name: rate-limiting
config:
minute: 20
policy: redis
redis:
host: redis
port: 6379
- name: bot-detection
config:
deny:
- "curl"
- "python-requests"
- "scrapy"
- name: jwt
config:
claims_to_verify: ["exp"]
- name: request-size-limiting
config:
allowed_payload_size: 8
The bot-detection plugin above is a coarse first pass — it catches unsophisticated scripts using default HTTP client user agents, which is a surprising fraction of low-effort credential stuffing traffic. Pair it with the fingerprinting from Step 4 for anything more determined.
If your API issues bearer tokens through an OAuth 2.0 flow as defined in RFC 6749, consider upgrading sensitive flows to sender-constrained tokens using DPoP, specified in RFC 9449. A stolen bearer token can be replayed from anywhere; a DPoP-bound token is cryptographically tied to the client that requested it, so a credential-stuffing bot that somehow captures a token in transit still cannot use it from a different device. This is a heavier lift than the other steps here and is best reserved for your highest-value endpoints — payments, account changes, admin APIs — rather than rolled out platform-wide on day one.
Step 9: Configure Account Lockout and Progressive Delays
Lockouts need to walk a line: strict enough to stop automated guessing, loose enough that an attacker cannot lock a real user out of their own account on purpose (a denial-of-service technique sometimes layered on top of credential stuffing). Progressive delays — where each failed attempt adds an increasing wait before the next one is accepted — are generally safer than hard lockouts, since they slow bots without fully denying access to a legitimate user who mistyped a password.
A workable pattern: no delay for the first two failed attempts, a 5-second delay for attempts three and four, 30 seconds for attempts five and six, and a temporary account-level hold (not a full lockout) after ten failures within an hour, which requires email or MFA verification to lift rather than a support ticket. Log every lockout event with the full context — IP, device fingerprint, timing — since a cluster of lockouts across many different accounts from a shared set of IPs is one of the clearest credential-stuffing signals you will get.
Step 10: Centralize Logging, Alerting, and SIEM Correlation
Every control above generates events — rate-limit trips, failed logins, breached-password flags, bot-detection blocks, anomaly-detection hits. None of it is useful scattered across five different systems' individual logs. Ship everything to a central pipeline with a consistent schema (source IP, account identifier, endpoint, outcome, risk score, timestamp) so a security analyst, or an automated rule, can correlate a single credential-stuffing campaign across every layer it touched.
At minimum, alert on: a sudden spike in 429 rate-limit responses from previously unseen IP ranges, a cluster of account lockouts within a short window, breached-password matches at a rate above your historical baseline, and any single IP or device fingerprint touching more than a handful of distinct accounts in an hour (real users do not do this; credential-stuffing bots do it constantly). Tune thresholds against your own traffic for at least two weeks before treating any of these as auto-blocking triggers, to avoid false-positive storms during legitimate traffic spikes like product launches.
Step 11: Red-Team Test Your Defenses
Defenses you have not tested are defenses you are guessing about. Run a controlled internal test using a small, disposable list of dummy credentials against your staging environment, simulating the exact pattern real credential-stuffing traffic uses: distributed source IPs (a VPN rotation is sufficient for testing purposes), varied user agents, and a realistic request rate rather than an obvious flood. Confirm that rate limiting trips at the expected threshold, that breached-password screening blocks known-bad passwords at signup, that MFA actually gates access after a correct password, and that your SIEM alerts fire within an acceptable window.
Document what got through and why. It is common to find that a secondary endpoint — a mobile API version, a partner integration, an internal admin tool — was left out of the hardening pass entirely. Re-run this test on a recurring schedule, ideally quarterly, since new endpoints and integrations get added constantly and each one is a fresh gap until it goes through the same 12 steps.
Step 12: Build the Incident Response Playbook
Even a well-defended API will eventually see a credential-stuffing campaign that partially succeeds. Have a playbook ready before that happens rather than improvising during an active incident. At minimum it should cover: how to force a password reset and session invalidation for a specific set of affected accounts without locking out your entire user base, how to temporarily tighten rate limits and challenge thresholds platform-wide during an active campaign, who has authority to make that call, and how you will notify affected users in a way that does not itself look like a phishing attempt.
Include a rollback plan for every emergency control you might enable — a rate limit tightened during an incident that never gets loosened afterward becomes a support burden for months. Assign an owner for post-incident review: which layer caught the attack, which layer should have caught it earlier, and what changes from this list of 12 steps need revisiting as a result.
Complete Working Project: A Credential-Stuffing-Resistant Login Middleware
Below is a condensed but complete Express.js middleware chain that combines the layered rate limiting from Step 2, breach-password screening from Step 5, and risk-based challenge routing from Steps 3 and 4 into a single working login flow. Adapt the risk-scoring function to your own bot-detection and fingerprinting provider.
// server.js — complete credential-stuffing-resistant login flow
const express = require('express');
const bcrypt = require('bcrypt');
const { credentialStuffingGuard } = require('./rateLimiter');
const { isPasswordBreached } = require('./checkBreachedPassword');
const { getRiskScore } = require('./riskEngine'); // your fingerprinting/bot provider
const app = express();
app.use(express.json());
app.post('/api/auth/signup', async (req, res) => {
const { email, password } = req.body;
const { breached, count } = await isPasswordBreached(password);
if (breached) {
return res.status(400).json({
error: 'password_breached',
message: `This password has appeared in ${count} known breaches. Choose a different one.`
});
}
const hash = await bcrypt.hash(password, 12);
const user = await createUser(email, hash);
return res.status(201).json({ id: user.id, email: user.email });
});
app.post('/api/auth/login', credentialStuffingGuard, async (req, res) => {
const { email, password } = req.body;
const deviceId = req.headers['x-device-id'];
const risk = await getRiskScore({ ip: req.ip, deviceId, email });
if (risk.score >= 80) {
return res.status(403).json({ error: 'blocked', reason: 'high_risk_request' });
}
if (risk.score >= 40) {
const challengePassed = await verifyProofOfWork(req.body.powToken);
if (!challengePassed) {
return res.status(428).json({ error: 'challenge_required' });
}
}
const user = await findUserByEmail(email);
const validPassword = user && await bcrypt.compare(password, user.passwordHash);
if (!user || !validPassword) {
await recordFailedAttempt(email, req.ip);
return res.status(401).json({ error: 'invalid_credentials' });
}
if (user.mfaEnabled) {
const mfaToken = await issueMfaChallenge(user.id);
return res.status(200).json({ mfaRequired: true, mfaToken });
}
const session = await createSession(user.id, { ip: req.ip, deviceId });
return res.status(200).json({ token: session.token });
});
app.listen(3000, () => console.log('Auth service running on port 3000'));
This flow rejects breached passwords at signup, applies layered rate limiting before any credential check runs, routes medium-risk requests through a proof-of-work challenge instead of blocking them outright, hard-blocks the highest-risk requests, and still requires MFA after a correct password for accounts that have it enabled. It intentionally returns generic error messages (invalid_credentials rather than "wrong password" or "user not found") to avoid the enumeration leak covered in Step 1.
Choosing Tools for Each Defense Layer
Every step above can be built with open-source components or bought as a managed service, and most teams end up mixing both depending on which layer carries the most risk. Rate limiting and breach-password screening are cheap enough to self-host from day one since the code involved is small and the external dependency (Redis, the HIBP API) is either free or nearly so. Bot detection and behavioral fingerprinting are the layers where buying tends to pay off faster, because the signal quality commercial vendors have built up from observing traffic across thousands of customers is difficult to replicate with an in-house team of any size.
| Defense layer | Self-hosted option | Managed/commercial option | Where it fits best |
|---|---|---|---|
| Rate limiting | Redis + custom middleware (Step 2) | API gateway plugin (Kong, AWS API Gateway, Apigee) | Every team, from day one |
| Breach-password screening | Have I Been Pwned API + local cache | Identity provider built-in (Okta, Entra ID, Auth0 password policies) | Every team, from day one |
| Bot/device fingerprinting | TLS JA3/JA4 fingerprint tracking | Cloudflare Bot Management, Akamai Bot Manager, DataDome, Arkose Labs | Teams with meaningful API traffic volume or prior incidents |
| MFA / passkeys | @simplewebauthn or platform WebAuthn libraries | Identity provider native passkey support | Every team, phased rollout |
| Anomaly detection / SIEM | ELK stack with custom detection queries | Datadog, Splunk, Microsoft Sentinel, or dedicated ITDR tooling | Teams with a dedicated security or platform function |
A useful rule of thumb: if a layer is failing open (letting traffic through) or failing closed (blocking legitimate users) in ways that generate support tickets or security incidents on a recurring basis, that is the layer worth paying for a managed solution. Layers that quietly do their job with the self-hosted version rarely need to change.
Common Pitfalls to Avoid
- Rolling out strict rate limits without a staging period. Tight limits deployed straight to production during a traffic spike (a marketing campaign, a product launch) will lock out real users and generate a support flood indistinguishable from an actual incident. Roll out gradually with monitoring first, enforcement second.
- Relying on IP reputation alone. With 89% of credential-stuffing traffic routing through residential proxies, IP-based blocking catches only the least sophisticated attackers. Treat it as one signal among several, never the sole gate.
- Leaking account existence through error messages or timing. A login endpoint that responds faster for a nonexistent email than a wrong password (because it skips the bcrypt comparison) leaks the same information a differently worded error message would. Normalize both the response and the response time.
- Forgetting the password-reset and MFA-resend endpoints. These get hardened last, if at all, precisely because they feel secondary — and attackers know it. Apply the same layered defense to every credential-adjacent route, not just the primary login.
- Treating MFA as binary instead of risk-based. Forcing every login through MFA regardless of risk creates friction that pushes users toward weaker, easier-to-remember-but-reused passwords elsewhere. Reserve mandatory step-up MFA for elevated-risk requests and sensitive actions, and make it optional-but-encouraged everywhere else.
- Blocking instead of challenging on medium-confidence signals. A hard block on anything less than high confidence generates false positives that cost you real users and support time. Use adaptive challenges as the middle tier, not a binary allow/deny.
Troubleshooting Guide
| Symptom | Likely cause | Fix |
|---|---|---|
| Real users getting 429 errors during normal use | Per-user or per-IP thresholds set too low for legitimate retry behavior | Raise the per-user threshold and check for shared-IP scenarios like corporate NAT or campus Wi-Fi |
| Rate limiter not triggering under simulated attack traffic | Redis key collisions or missing X-Forwarded-For handling behind a load balancer | Verify the load balancer forwards the real client IP and that Redis keys are scoped correctly per endpoint |
| Breach-password API calls timing out | Network egress blocked or rate limits on the HIBP API itself | Cache negative and positive results locally for a short TTL and add a circuit breaker with a fail-open policy for signup availability |
| Passkey registration fails silently | Origin or RP ID mismatch between frontend and backend configuration | Confirm expectedOrigin and expectedRPID exactly match the deployed domain, including scheme and absence of trailing slash |
| Bot-detection plugin blocking legitimate mobile app traffic | Custom user agents or SDK HTTP clients matching overly broad deny patterns | Whitelist your own app's known user-agent strings and rely on fingerprinting rather than user-agent string matching alone |
| SIEM alerts flooding on impossible-travel detection | Users on corporate VPNs or mobile carriers that rotate egress IPs across regions | Raise the distinct-country threshold and factor in ASN reputation rather than raw IP geolocation |
| Account lockouts spiking without matching attack traffic | A third-party integration or internal script retrying with stale credentials | Audit service accounts and API keys for expired credentials still being retried on a loop |
| MFA challenge not appearing after a correct password | Session creation logic bypassing the MFA branch due to a caching or race condition | Add an explicit test case for the MFA branch in your auth test suite and verify session tokens are never issued before MFA completes |
| Progressive delay not slowing down scripted attempts | Delay implemented client-side instead of server-enforced | Move all delay and backoff enforcement server-side; a client-side delay is trivially bypassed by any scripted client |
Advanced Tips for Mature Security Teams
Once the 12 core steps are in place, a few advanced moves squeeze out meaningfully more protection. First, integrate breach-monitoring feeds directly into your identity provider so that accounts whose credentials appear in a new breach dump get force-reset proactively, rather than waiting for an attacker to try them against you first — several startup security guides published in 2026 recommend this as a standard control rather than a nice-to-have. Second, if you operate infrastructure with AI agents or automation tooling that call your APIs on users' behalf, audit those tools' own credential handling: a recent disclosure found that AWS Strands Agents Tools, a package for the Strands Agents SDK, received a string of security advisories between July 15 and August 6, 2026 tied to credential exfiltration and consent-gate issues, including CVE-2026-15746 (CVSS 6.5) in its elasticsearch_memory tool, CVE-2026-18394 (CVSS 6.9) in its http_request tool, and CVE-2026-18733 (CVSS 8.8) in its shell tool. The lesson generalizes: automation and agent tooling that stores or passes API credentials needs the same scrutiny as your human-facing login flow.
Third, correlate credential-stuffing signals with your broader threat intelligence rather than treating each API as an island — an attacker testing credentials against your login API is very likely testing the same list against your partners' APIs simultaneously, and sharing indicators (blocklisted IP ranges, known-bad device fingerprints) across a trusted network multiplies the value of everyone's individual defenses. Finally, revisit your rate-limit and risk thresholds every quarter using real production data rather than leaving Step 2's initial numbers untouched indefinitely; attacker behavior adapts, and a threshold tuned correctly in January can be trivially routed around by August.
Frequently Asked Questions
Is credential stuffing the same as a brute-force attack?
No. Brute forcing guesses passwords without prior knowledge; credential stuffing replays real username-password pairs already exposed in a previous, unrelated breach. They require different primary defenses, though rate limiting and MFA help against both.
Will rate limiting alone stop credential stuffing?
No. Rate limiting slows attacks and stops the least sophisticated bots, but with the majority of credential-stuffing traffic routing through residential proxies, distributed attacks can stay under per-IP thresholds. Rate limiting is one layer among the 12 steps here, not a complete solution on its own.
Do I need a commercial bot-detection product, or can I build this myself?
You can build a credible baseline yourself using the techniques in this tutorial — layered rate limiting, breach-password screening, and basic fingerprinting. Commercial products (Cloudflare, Akamai, DataDome, Arkose Labs) add more sophisticated device and behavioral signals that are hard to replicate in-house, and are worth evaluating once your API traffic and risk profile justify the cost.
How often should breach-password screening run?
At signup and password change in real time, using the k-anonymity Have I Been Pwned API. For your existing user base, run it as a periodic offline batch job rather than checking on every login, since you should not be comparing plaintext passwords against an external API during live authentication.
Are passkeys really immune to credential stuffing?
Yes, in the sense that matters here: passkeys have no shared secret that can be stolen from a different service and replayed against yours. Each passkey is a cryptographic key pair bound to your specific domain, so there is no password to stuff in the first place.
What is a reasonable rate-limit threshold for a login endpoint?
A commonly cited starting point is roughly 5-10 attempts per user per minute and 15-20 attempts per IP per minute, tightened further for password-reset and MFA-resend endpoints. Treat these as starting points to tune against your own traffic, not fixed rules.
Can credential stuffing succeed even with a strong password policy?
Yes. Complexity requirements do nothing to stop reuse of an already-strong password that was exposed in a breach of a completely unrelated service. That is exactly why breach-password screening, not complexity rules, is the relevant password-side control against this specific attack.
How do I know if my API is already under a credential-stuffing attack?
Watch for a spike in failed login attempts distributed across many different accounts and source IPs, a cluster of account lockouts in a short window, and login attempts with usernames that do not correspond to any real account (attackers testing lists against you blindly). The logging and SIEM correlation from Step 10 is what surfaces these patterns in practice.


