Detection Engineering

Credential Stuffing Detection

Credential stuffing detection that finds what hides under per-IP thresholds. Success-rate inversion, low-and-slow campaigns, and ATO signals in SPL and KQL.

A dark grid of cyan login attempts with a sparse red pattern spread thinly across it, representing credential stuffing detection

Credential stuffing detection fails in most organisations for a specific and fixable reason: the detection is built per source IP, and the attack is built to stay under per-source thresholds. A modern stuffing campaign spreads across tens of thousands of residential proxy addresses, makes two or three attempts from each, and looks exactly like a large population of users who mistyped a password. Every individual request is unremarkable. Only the population is anomalous. This guide detects the population.

Credential stuffing maps to MITRE ATT&CK T1110.004 — Brute Force: Credential Stuffing and leads directly to T1078 — Valid Accounts. It is the volume attack against every authentication endpoint on the internet, and the one most often invisible in logs that are technically complete.

What is credential stuffing?

Credential stuffing is the automated replay of username and password pairs stolen from one breach against a different service. It is not brute force and the distinction is operationally important. Brute force guesses; stuffing replays pairs that were already valid somewhere, betting entirely on password reuse.

That bet pays at rates most defenders underestimate. Published success rates cluster somewhere around 0.1 to 2 percent depending on the credential list’s freshness and the target’s demographics. Against a list of ten million pairs, even the low end of that range yields thousands of compromised accounts, and the marginal cost of each attempt is close to zero.

The economics explain the shape of the attack. The attacker is not trying to break one account; they are running a conversion funnel. They do not care about your specific defences so much as about throughput and cost per validated credential, which is why the countermeasure that works is making the attack expensive rather than making any single attempt fail.

AttackWhat variesWhat is constantDetection angle
Brute forcePasswordOne accountFailures per account
Password sprayAccountOne common passwordOne password, many accounts
Credential stuffingBothNeitherPopulation-level anomaly
Credential crackingPassword offlineThe stolen hashNot visible in your logs

The third row is the difficult one. Because both the account and the password vary on every attempt, none of the per-entity thresholds that catch the other three have anything to grip. This is why stuffing detection is a fundamentally different problem from password spraying detection and why reusing those analytics produces silence.

What does a credential stuffing campaign look like over time?

Campaigns are not a single undifferentiated flood, and recognising the stage you are looking at changes both the urgency and the correct response. Most run through four phases, each with a different signature.

PhaseWhat the attacker doesDurationDominant signalYour best action
ReconnaissanceProbes the endpoint, measures responses and timingMinutes to hoursAccount enumeration attempts, odd endpoint pathsClose enumeration oracles
Validation probeTests a small sample to gauge hit rateMinutesSmall volume, high nonexistent-account ratioFingerprint the tooling early
Full runWorks the list at scale across the proxy poolHours to daysSuccess-rate inversion, high distinct-IP countGraduated friction
MonetisationLogs into validated accounts and actsDays to weeksPost-login takeover behaviourReset and revoke

The validation probe is the phase worth investing detection effort in, because it is the cheapest moment to intervene and the one almost nobody catches. An attacker with a fresh list of several million pairs will not spend the proxy budget to run all of it against an unknown target. They test a few hundred first to measure the hit rate, and if the hit rate is poor they move to a different target entirely. That probe is small, brief, and generates nowhere near enough volume to move an aggregate success rate.

What it does generate is a distinctive client fingerprint arriving from a handful of addresses in a tight time window, which is exactly what a fingerprint-uniformity analytic catches. Flag that fingerprint and you can apply friction to the full run before it starts, at the moment your cost of response is lowest and theirs is highest.

The monetisation phase deserves separate emphasis because it frequently arrives long after the authentication phase has ended and been forgotten. Validated credential lists are traded, so the party who ran the stuffing is often not the party who eventually uses the accounts. A campaign you detected and considered closed six weeks ago can produce takeovers now, which is why the post-login behavioural analytic below runs continuously rather than only during an active incident.

What telemetry do you need?

Population-level detection requires fields most authentication logs do not carry, and the gaps are consistent across organisations.

RequirementWhy it mattersCommon failure
Every attempt logged, success and failureSuccess rate is a ratio; you need both termsOnly failures logged
Attempted username on failuresEnables one-credential-many-accounts detectionRedacted for privacy
True client IP plus resolved ASNDistinguishes residential proxy poolsOverwritten by the CDN or LB
Client fingerprint (TLS/JA3, header order)Catches uniform tooling across many IPsNot captured at all
Failure reason codeSeparates wrong-password from no-such-userCollapsed into a generic failure
Post-login action eventsLinks stuffing to actual takeoverAuth and app logs not correlated
One-minute or finer aggregationBursts hide inside hourly rollupsHourly-only metrics

Two of these deserve attention. Logging only failures is extremely common and it makes the single best analytic impossible, since a success rate needs a denominator. And collapsing the failure reason destroys a genuinely valuable signal: a wrong password on a real account and an attempt against a nonexistent account mean very different things, and the ratio between them tells you whether the attacker’s list is targeted at your platform or generic.

How to detect credential stuffing

Three analytics: one for the attack in progress, one for the credential list being worked, and one for the takeover that follows.

Success-rate inversion across the source population

The most reliable signal available. Healthy login traffic succeeds most of the time. Stuffing traffic fails almost all of the time, and when it arrives in volume it drags the aggregate ratio down sharply even though no individual source looks unusual.

SPL Success-Rate Inversion Across a Distributed Source Population
index=auth sourcetype=app:login_attempt earliest=-60m
| bin _time span=1m
| stats count AS attempts,
        sum(eval(result=="success")) AS successes,
        dc(src_ip) AS distinct_ips,
        dc(username) AS distinct_users,
        dc(src_asn) AS distinct_asns
        by _time
| eval success_rate = round(successes / attempts * 100, 2)
| eval attempts_per_ip = round(attempts / distinct_ips, 2)
| eventstats avg(success_rate) AS baseline_rate,
             stdev(success_rate) AS baseline_stdev
| eval z_score = round((baseline_rate - success_rate) / baseline_stdev, 2)
| where success_rate < 25
    AND distinct_ips > 200
    AND attempts_per_ip < 5
    AND z_score > 3
| table _time, attempts, successes, success_rate, baseline_rate, z_score,
        distinct_ips, distinct_users, distinct_asns, attempts_per_ip
| sort _time

The attempts_per_ip < 5 clause is what makes this specific to distributed stuffing rather than to a single broken client hammering the endpoint. Combined with a high distinct-IP count and a collapsed success rate, it describes exactly one thing. Derive the success_rate < 25 threshold from your own baseline rather than adopting it directly, since a platform with an unusual login flow may sit lower normally.

One credential attempted across many accounts

The complementary view. Where success-rate inversion catches volume, this catches the attacker methodically working a list, including the low-and-slow campaigns that never generate enough volume to move an aggregate ratio.

KQL Single Credential Attempted Across Many Accounts Over a Long Window
let lookback = 7d;
let min_accounts = 15;
SigninLogs
| where TimeGenerated > ago(lookback)
| where ResultType != 0
| extend ClientIP = tostring(IPAddress),
         ASN = tostring(NetworkLocationDetails),
         UA = tostring(UserAgent)
| summarize AttemptedAccounts = dcount(UserPrincipalName),
            TotalAttempts = count(),
            SourceIPs = dcount(ClientIP),
            ASNs = dcount(ASN),
            UserAgents = dcount(UA),
            Window = max(TimeGenerated) - min(TimeGenerated),
            SampleAccounts = make_set(UserPrincipalName, 15)
        by CredentialHash = tostring(hash_sha256(strcat(UserPrincipalName, ClientAppUsed)))
| where AttemptedAccounts >= min_accounts
| extend AttemptsPerIP = round(todouble(TotalAttempts) / SourceIPs, 2),
         AccountsPerHour = round(todouble(AttemptedAccounts) /
                                 (Window / 1h), 2)
| where AttemptsPerIP < 5
| project CredentialHash, AttemptedAccounts, TotalAttempts, SourceIPs, ASNs,
          UserAgents, AttemptsPerIP, AccountsPerHour, Window, SampleAccounts
| order by AttemptedAccounts desc

The seven-day lookback is deliberate and it is the point of this rule. A low-and-slow campaign touching fifteen accounts over five days generates nothing on any hourly window and is plainly visible here. Note the UserAgents count: a genuine population of forgetful users produces dozens of distinct user-agent strings, while tooling frequently produces one or two across thousands of source addresses. That uniformity across otherwise-diverse IPs is one of the hardest properties for an attacker to fix cheaply.

Post-authentication account takeover behaviour

Stuffing that succeeds becomes account takeover, and takeover has its own behavioural signature that is often clearer than the authentication signal that preceded it.

SPL Post-Authentication Account Takeover Behaviour
index=auth sourcetype=app:login_attempt result=success earliest=-24h
| join type=inner user_id
    [ search index=app sourcetype=app:account_action earliest=-24h
      | search action IN ("email_change","password_change","mfa_enroll",
                          "mfa_remove","api_key_create","payout_change",
                          "recovery_email_change","session_export")
      | fields user_id, action, _time AS action_time, src_ip AS action_ip ]
| eval seconds_to_action = action_time - _time
| where seconds_to_action >= 0 AND seconds_to_action < 900
| lookup user_baseline user_id OUTPUT usual_asn, usual_country, account_age_days
| eval risk = 0
| eval risk = risk + if(src_asn != usual_asn, 2, 0)
| eval risk = risk + if(src_country != usual_country, 2, 0)
| eval risk = risk + if(action IN ("mfa_remove","payout_change","api_key_create"), 3, 0)
| eval risk = risk + if(seconds_to_action < 120, 2, 0)
| eval risk = risk + if(account_age_days > 180 AND src_country != usual_country, 1, 0)
| where risk >= 5
| table _time, user_id, action, seconds_to_action, src_ip, src_asn,
        src_country, usual_country, risk
| sort - risk

The fifteen-minute window matters. A legitimate user who logs in and changes their email does it eventually; an attacker validating and monetising a compromised account does it immediately, because they are working through a list. Scoring rather than single-signal alerting keeps this usable, and mfa_remove alongside a country change is close to definitive.

Which false positives will you actually see?

False positiveWhich rule it hitsWhy it happensResolution
Corporate NAT egressSuccess-rate inversionMany users behind one IPExclude known corporate ASNs from per-IP maths
Mobile app token refresh stormSuccess-rate inversionApp bug retries auth in a loopSeparate interactive from programmatic auth
Password expiry waveSuccess-rate inversionPolicy forces mass resetsCorrelate with policy events, suppress the window
Shared family accountsATO behaviourGenuinely multiple locationsBaseline per account, not per population
Traveling usersATO behaviourReal country changeRequire a second risk signal, not geography alone
SSO misconfigurationOne-credential-manyA broken integration retries widelyExclude service principals
Security scannerAll threeAuthorized testingAllowlist scanner ranges and coordinate windows
Post-breach forced resetSuccess-rate inversionYour own remediationSuppress during declared reset campaigns

The mobile-app row is the one that most often causes teams to disable the detection. A client bug that retries authentication aggressively looks statistically similar to an attack, and the fix is to separate interactive login attempts from programmatic token refresh at the log level, not to raise the threshold until both disappear.

How do you triage a credential stuffing campaign?

  1. Confirm it is stuffing, not spraying. Check whether the attempted passwords vary. If one password dominates, it is spraying and the response differs.
  2. Measure the validated set. Identify which accounts succeeded. That list is the actual incident; the failures are noise around it.
  3. Force reset the succeeded accounts and revoke their sessions, following the session revocation path so a stolen session does not survive the password change.
  4. Check each succeeded account for takeover actions in the fifteen minutes after login, using the analytic above. Persistence beats the password reset otherwise.
  5. Characterise the source population. ASN distribution and fingerprint uniformity tell you whether this is a commodity proxy pool or something more targeted.
  6. Determine list provenance if you can. A high nonexistent-account ratio means a generic breach dump; a low one means the list was built against you and there may be an earlier incident you missed.
  7. Deploy graduated friction rather than a block. Blocking the pool invites rotation. Friction raises cost without telling the attacker precisely what tripped.
  8. Watch the recovery flows. Blocked at login, attackers move to password reset and MFA recovery. That is the next surface, and it is usually weaker.

Step 8 is the most-missed. A well-defended login endpoint routinely displaces the attack onto an account-recovery flow that was designed for user convenience and never threat-modelled.

How to test your credential stuffing detection

Against a lab environment you own or are explicitly authorized to test, using synthetic accounts and synthetic credential pairs only:

  1. Generate distributed low-volume failed logins across many source addresses. Confirm success-rate inversion fires while per-IP rate limiting stays silent, which demonstrates the gap the analytic exists to close.
  2. Attempt one synthetic credential across twenty synthetic accounts over several days. Confirm the seven-day KQL rule catches it.
  3. Successfully authenticate to a synthetic account and immediately remove MFA. Confirm the ATO analytic scores it above threshold.
  4. Log in from a new country without any sensitive action. Confirm this does not alert, so you know geography alone is not driving the rule.
  5. Replay a corporate NAT pattern and confirm the exclusion suppresses it.
  6. Verify the client IP field is the real client and not your edge, which is the failure that silently disables every analytic here.

How to prevent credential stuffing

Four additional controls matter more than their implementation cost suggests:

  • Never leak account existence. Identical response bodies and identical timing on “no such user” and “wrong password.” Emit the distinction to your SIEM, never to the client.
  • Rate limit on the population, not the source. Adaptive limits keyed on aggregate success rate hold when per-IP limits do not.
  • Treat validated-credential discovery as its own incident. Even where MFA blocked takeover, the attacker learned which pairs are valid, and that list has value.
  • Monitor for your users’ credentials in breach corpora and force resets proactively.

Which anti-stuffing control should you deploy first?

ControlStopsEffortUser frictionDeploy when
Breached-credential screeningReuse at the rootLowNoneAlways, first
Population-level detectionThe campaign itselfLowNoneAlways, second
Graduated friction / challengeThroughputMediumLow, risk-scaledYou have volume
Passkeys / phishing-resistant MFATakeover entirelyMediumLow once enrolledAlways, in parallel
Client attestation at edgeCheap toolingMediumNoneBot pressure is sustained
Hardened recovery flowsThe displaced attackMediumLowYou deployed the above

Breached-credential screening is the highest-leverage item and the most frequently skipped. It is cheap, invisible to legitimate users, and it attacks the precondition the entire attack depends on rather than its symptoms.

Common credential stuffing detection mistakes

  • Relying on per-IP rate limiting. It measures the wrong unit for a distributed attack.
  • Logging only failed logins. A success rate needs both terms.
  • Collapsing the failure reason. You lose the list-provenance signal.
  • Alerting on geography alone. Users travel; require a second risk signal.
  • Blocking the proxy pool outright. It rotates, and you lose visibility.
  • Stopping at the password reset. Check for MFA changes and API keys first.
  • Treating MFA as the whole answer. The validated list still has value, and recovery flows become the target.
  • Never testing with the client behind a proxy. That is where the client-IP field breaks.

Credential stuffing detection checklist

  1. Log every authentication attempt with its outcome, not only failures.
  2. Emit a distinct failure reason for nonexistent account versus wrong password, to the SIEM only.
  3. Confirm the logged client IP is the real client and not your load balancer or CDN.
  4. Capture a client fingerprint (TLS or header order) alongside the IP.
  5. Baseline your normal login success rate at one-minute granularity.
  6. Alert on aggregate success-rate inversion with a high distinct-IP and low attempts-per-IP shape.
  7. Run a seven-day one-credential-many-accounts analytic to catch low-and-slow campaigns.
  8. Correlate successful logins with account actions in the following fifteen minutes.
  9. Score takeover risk rather than alerting on any single signal.
  10. Exclude corporate NAT ranges and separate programmatic auth from interactive login.
  11. Screen passwords against breached-credential corpora at set and at change.
  12. Return identical responses and timing for nonexistent and wrong-password cases.
  13. Deploy graduated friction keyed on population risk rather than hard blocks.
  14. Threat-model and instrument password reset and MFA recovery as attack surfaces.
  15. Treat discovery of a validated credential set as an incident even when MFA held.

The takeaway

Credential stuffing detection works when it stops measuring individual sources and starts measuring the population. Aggregate success-rate inversion catches the campaign, a long-window one-credential-many-accounts analytic catches the patient version, and post-login behavioural scoring catches what actually matters, which is takeover. Then attack the economics: screen breached passwords at the root, deploy phishing-resistant MFA, add friction that scales with risk, and harden the recovery flows the attacker moves to next. Continue with session hijacking detection, JWT misconfiguration detection, and best password managers for security teams, or browse the full Detection Engineering pillar.

Training & tools referenced

Disclosure: Some links below are affiliate links. If you buy through them, darkpwn may earn a commission at no extra cost to you. We only recommend training and tools we actually use in our own lab, and affiliate links never influence editorial coverage.

  • TryHackMeAuthorized labs to practice authentication attack analysis and API defenseSecurity Training
    Start training
  • 1PasswordUnique-password enforcement is the root fix for password reusePassword Manager
    Compare plans

Frequently asked questions

What is credential stuffing?

Credential stuffing is the automated replay of username and password pairs stolen from one breach against a different service, betting on password reuse. It is not guessing. Every pair was valid somewhere, which is why success rates of roughly 0.1 to 2 percent still yield thousands of compromised accounts when millions of pairs are tried.

How do you detect credential stuffing?

Detect the population, not the individual request. A distributed attack keeps every source under your per-IP threshold, so per-IP rate limiting sees nothing. The reliable signals are aggregate success-rate inversion, one credential attempted across many accounts, unusually uniform client fingerprints across many IPs, and account-takeover behaviour immediately following a successful login.

Why does rate limiting not stop credential stuffing?

Because rate limiting is almost always applied per source IP, and modern credential stuffing distributes across tens of thousands of residential proxy addresses. Each address makes two or three attempts, which is indistinguishable from a forgetful user. The attack is only visible when you aggregate across sources rather than limiting within them.

What is a normal login success rate?

Most consumer platforms sit somewhere between 75 and 95 percent success on login attempts, and the exact figure matters less than its stability. Credential stuffing drives the aggregate rate sharply down because the overwhelming majority of replayed pairs fail. A sudden inversion in that ratio is one of the most reliable signals available.

Does MFA stop credential stuffing?

MFA stops the takeover but not the attack, and the distinction matters operationally. The attacker still learns which credential pairs are valid on your platform, and that validated list is itself sellable and reusable against your MFA recovery flows. Detect and block the stuffing even where MFA holds, and treat recovery paths as part of the attack surface.