Spotting credential stuffing attacks in your error stream
Auth-error rate, geo dispersion, and user-agent patterns that distinguish a real attack from a buggy mobile app release.
It's 3:00 AM and your error tracker is screaming. You see a 400% spike in AuthenticationError exceptions. Is it the React Native update you pushed yesterday failing to refresh tokens? Or is a botnet currently cycling through a 10-million-row combo list of stolen passwords against your /api/login endpoint? If you can't tell the difference in under five minutes, you're either about to wake up the wrong engineering team or let a massive account takeover (ATO) event run unnoticed.
Most teams read an error spike as "something is broken." On auth endpoints, a spike very often means "someone is attacking." The good news is that your error tracker already captures the metadata you need to tell those two stories apart—IPs, user agents, geo, breadcrumbs, release tags. You just have to know which signals to read. This post is the field guide.
The Error Stream as a Security Sensor
WAF logs see traffic; error trackers see intent. A WAF records that IP 203.0.113.5 hit /api/login and got a 401. Your error tracker records that, plus the user-agent string, the device metadata, the breadcrumb trail leading up to the attempt, and whether a real client-side exception fired. That context is what turns a number into a diagnosis.
Why the 401 is your most important security signal
A 401 Unauthorized is the cleanest indicator of a brute-force or credential-stuffing attempt, because it represents a credential that didn't work. One 401 is a typo. Ten thousand 401s in a minute, fanned across hundreds of source IPs, is reconnaissance and exploitation in progress. The OWASP Automated Threats handbook formalizes this as OAT-008 (credential stuffing), and the 401/403 rate is its primary tell.
The visibility gap
Standard log aggregators flatten this into counts. A WAF entry tells you IP and URL. Your error tracker can tell you which usernames were tried, what the device fingerprint looked like, and whether the request arrived with the breadcrumbs a real browsing session leaves behind. That difference—context versus counting—is the whole game when you're deciding whether to page security or roll back a deploy.
Anatomy of a Credential Stuffing Spike
Automated attacks have a shape. Once you've seen it, it's hard to unsee.
Volume vs. velocity
A bug ramps; an attack switches on. Real software failures tend to climb as a release rolls out to more users. A botnet initiation is a vertical wall—zero to thousands of auth failures per minute in a single step, because the operator just pressed go on a fixed list.
The uniformity trap
Real users are messy. They're on a hundred different browser versions, OSes, and screen sizes. Bots are uniform: thousands of "distinct" IPs all presenting the identical user-agent string, identical header ordering, identical Accept-Language. When your high-cardinality dimensions (user-agent, device) collapse to one value while your IP count explodes, that's a fleet, not a customer base.
The success/failure ratio
Here's the counterintuitive part: a 0.1% success rate is terrifying, not reassuring. Credential stuffing works precisely because a small fraction of reused passwords hit. If 0.1% of a 10-million-row list validates, that's 10,000 compromised accounts. A low success rate paired with enormous volume is the signature of a working attack, not a failing one.
// A suspicious error payload — note the static, outdated UA
{
"level": "error",
"message": "AuthenticationError: invalid credentials",
"request": { "url": "/api/login", "method": "POST" },
"tags": {
"status_code": "401",
"geo": "RU",
"user_agent": "Mozilla/5.0 (Windows NT 6.1) Chrome/80.0.3987.100"
},
"breadcrumbs": [] // no page load, no clicks — bots don't browse
}Fingerprinting the Attack: Three Key Patterns
Geo-dispersion
Tag every auth event with CF-IPCountry or equivalent geo metadata. If your app serves one country but suddenly "users" from 40 countries are very interested in your login endpoint, that's distributed infrastructure, not organic growth. The geographic spread of a stuffing campaign almost never matches your real customer distribution.
User-agent entropy
Watch for low entropy in a place that should have high entropy. Thousands of requests sharing one UA is a flag. So is an anachronistic UA—Chrome 80 in 2026, or a raw HeadlessChrome string—which indicates a script-driven client that never updated. Real browser populations drift forward constantly; bot fleets are frozen at whatever version the operator scripted.
Breadcrumb trails
This is the strongest single tell. Real users browse: a homepage load, a click on "Sign in," a focus event on the email field, then the POST. Bots hit /api/login cold. Missing breadcrumbs—no preceding navigation, no interaction events—mean the request didn't come from a human using your UI. Capturing and watching these trails (without recording the passwords inside them—see masking PII in session replay) is how you separate a person from a script.
Attack vs. Bug: How to Spot the "Bad Release"
The expensive mistake runs the other direction too: treating your own broken deploy as an attack and escalating to security at 3 AM.
The token-expiry loop
A bug in interceptors.ts—say a refresh-token flow that retries forever on a 401—produces a beautiful imitation of brute force. Thousands of auth failures, rapid cadence, real users. The difference is that these come from your clients failing, not external credentials being tested.
Version correlation
The fastest disambiguator is the release tag. If 100% of the auth errors carry the same release (v1.2.4), it's a bug in that build. If they span every version your users run, no single deploy can explain them—it's external. This one check resolves most 3 AM ambiguity in seconds, which is exactly why our post on bad-deploy error spikes leans on it so heavily.
Client-side stack traces
Real bugs throw real exceptions with real client-side stack traces. Bot attacks usually generate server-side 401s with no corresponding client exception, because there's no browser running your code—just a script POSTing JSON. Presence of a genuine stack trace leans bug; absence of one leans attack.
The Financial Impact: When the Attack Becomes a Bill
The double whammy
On a per-event pricing model, a credential-stuffing attack is a financial DDoS on top of a security one. A botnet generating a million auth errors in a few hours can burn your entire monthly event quota before lunch. You get attacked, and then you get invoiced for observing the attack.
Why flat-rate is a security feature
The instinct under per-event pricing is to drop auth errors to control cost. That is precisely the wrong move during an incident—you're blinding your best sensor to save money. Flat-rate pricing (the model behind our pricing argument) means an attack costs the same as a quiet Sunday, so you can keep full visibility through the spike instead of rationing it.
// The DANGEROUS "fix" people reach for under per-event pricing:
Sentry.init({
beforeSend(event) {
if (event.tags?.status_code === '401' && rateExceeded()) {
return null; // dropping the exact events that signal the attack
}
return event;
},
});Dropping 401s to save quota means that during the one event where visibility matters most, you have none. Don't build your blind spot on purpose.
Automating the Defense with Alerts
Turn the error stream into a tripwire. Configure issue-change alerts on auth-specific fingerprints so a new pattern of AuthenticationError with low UA entropy escalates on its own. Use threshold alerting tied to a rate—auth failures exceeding, say, 5% of total traffic—rather than a raw count that fires every Black Friday. And route confirmed auth spikes to your security or SOC channel, not the general engineering channel, so the right humans see it first. The full heuristic design, including how to avoid paging on legitimate surges, is in auth spike anomaly detection.
Mitigation: From Detection to Blocking
Once the stream confirms the attack, act. Put a challenge—Cloudflare Turnstile or a targeted WAF rule—specifically on /api/auth/* so you raise friction for bots without breaking the rest of the app. Rate-limit thoughtfully: per-IP limiting catches the lazy single-source brute force, but distributed stuffing needs per-username limiting, since the attack spreads one attempt across thousands of IPs. And for the 0.1% of logins that succeeded during the spike, force password resets and revoke sessions—those are your actual breached accounts. Our security headers checker and security docs cover hardening the surrounding surface.
Your error tracker is already a security sensor; the metadata to catch credential stuffing is sitting in events you're probably about to filter out. Read the user-agent entropy, the geo dispersion, the missing breadcrumbs, and the release tag, and you can call attack-versus-bug in five minutes instead of fifty. Stop paying for the privilege of being attacked—GlitchReplay's flat-rate tracking lets you keep every auth signal through a spike without fearing a surprise bill.
GlitchReplay is Sentry-SDK compatible, includes session replay and security signals, and never charges per event. Free to start, five minutes to first event.