Guide
WordPress login attempt surge — distinguishing credential stuffing from scanner traffic
1. Problem
Your WordPress site is hammered with login attempts. The auth log is rolling. wp_auth_failures_total jumped from a flat baseline to thousands per minute somewhere in the last hour. PHP-FPM workers are starting to pin. The marketing dashboard graphs are still green.
You searched "wordpress hammered with login attempts what to do" or "wordpress credential stuffing vs scanner" and landed here because every other page told you the same thing: install a security plugin, enable 2FA, rate-limit /wp-login.php. None of that answers the actual question you have right now, which is: what kind of attack is this, and is it dangerous?
Two surges that look identical in wp_auth_failures_total can mean wildly different things. A scanner sweeping every WordPress site is noise. A targeted credential-stuffing campaign with a leaked dump is a breach in progress. The failed-login count doesn't tell you which one. The shape of the surge does.
Same primary signal — wp_auth_attempts_total — four very different attack classes, each with its own fingerprint and its own response.
2. Impact
Treating every login surge the same way is how breaches happen:
- Credential stuffing read as scanner noise. You blanket-block the top 50 IPs by failed-attempt count. The attack is distributed across 4,000 residential IPs at 2 attempts each. You blocked 0.3%. A week later a credential works and a customer's account drains.
- Scanner sweeps read as targeted attacks. You wake the on-call, page security, force-rotate every admin password at 3 AM. It was the same
WPScan/3.8.22user-agent that hits every WordPress site on Tuesdays. Team exhausted before the real incident on Thursday. - Password-spray hidden in scanner noise. A real attacker hides 200 targeted attempts/hour inside 50,000 scanner attempts/hour. You see only the volume; the spray is invisible. They get in.
- Operator lockout misread as attack. An admin forgot their password and is hammering the form. You ban their office IP. Trust damage, no security gain.
A single wp_auth_failures_total count of "12,000 failures in 30 minutes" is consistent with all four. The remediation differs by an order of magnitude. Shape determines which one you're paying for.
3. Why It’s Hard to Spot
WordPress logs the fact of a failed login. It does not log the shape of the attack.
wp-login.php writes nothing to the database for failures by default. The audit-log plugins that do log them tend to record per-event rows: timestamp, user, IP, result. To see the shape you have to aggregate — group by IP, group by username, cross-tab IP × username, look at the rate-of-change, look at the success ratio. None of that is in any WordPress admin screen.
Security plugins surface the wrong primitive. They show you "top attacking IPs" and "blocked attempts last 24h." Top-IP rankings hide distributed attacks by definition: when 4,000 IPs each try 2 passwords, no IP makes the top list. "Total blocked" is a counter, not a shape.
Uptime monitors miss the whole thing. The login form returns 200 OK on a failed attempt. From outside, your site is healthy. From inside, you don't know if the failures are 1 IP times 50,000 attempts (loud, mostly harmless) or 5,000 IPs times 10 attempts (quiet, possibly catastrophic).
The four attack classes feel ghosty for the same reason: WordPress shows you that logins failed, never how they're distributed.
4. Cause
Logystera publishes wp_auth_attempts_total for every authentication attempt across wp-login.php, REST, and XML-RPC, with labels for result, username, source_ip, and ip_country. Combined with wp_auth_failures_total, wp_logged_in_requests_total, and wp_request_fingerprint_top, you can compute the surge shape in real time.
Pre-computed insight wp_auth_attempt_surge watches three derived numbers:
- Rate-of-change of
wp_auth_attempts_total— how steep is the climb against the last 24-hour baseline. - Success ratio —
success / (success + failure)over the surge window. - Cardinality cross of
source_ip×username— distinct IPs, distinct usernames, and the relationship between the two.
Those three numbers, computed once per minute on the processor side, form a fingerprint. The fingerprint resolves to one of the four shapes below. Each shape is a different attack class with a different remediation. Same primary signal, four distinct stories.
5. Solution
5.1 Diagnose the shape (logs first)
Step one is always to confirm the surge is real and pull the raw failure stream into a window you can group. Start with the access log so you have IP and timestamp; the auth signal gives you username and result.
# Confirm the surge is real and find its start time
grep "POST /wp-login.php" /var/log/nginx/access.log \
| awk '{print $4}' | cut -c2-18 | sort | uniq -c
# → counts attempts per minute. Surfaces wp_auth_attempts_total{result="failure"}
# rate-of-change. Look for the inflection point — that's the surge start.
Time-correlate that inflection point with anything that changed:
# Did the surge start right after a public mention, a deploy, or a CVE drop?
grep "$(date -d '2 hours ago' '+%Y-%m-%dT%H')" /var/log/wp-audit.log
# → ties the wp_auth_attempts_total spike to the triggering event.
# Common triggers: HaveIBeenPwned dump posted, your site mentioned in
# a high-traffic article, a new WP plugin CVE, a scheduled scanner run.
Now extract the shape. The single most diagnostic command is the IP × username cross-tab over the surge window:
# Pull the auth log for the last hour, group by IP and username
awk '/wp-login.php/ && /401|failure/ {print $1, $7}' \
/var/log/wp-audit.log \
| sort | uniq -c | sort -rn | head -50
# → reveals wp_auth_failures_total cardinality:
# - few rows, huge counts → one IP many users (Shape A) or one IP one user (Shape D)
# - many rows, count of 1-2 each → many IPs many users (Shape C)
# - one username dominates across many IPs → Shape B
Cross-check the success ratio. A surge with any non-zero success during failure storm is the alarm bell:
# Among the failures, did anything succeed?
grep "$(date '+%Y-%m-%dT%H')" /var/log/wp-audit.log \
| grep -E "result=(success|failure)" \
| awk -F'result=' '{print $2}' | awk '{print $1}' | sort | uniq -c
# → wp_auth_attempts_total{result="success"} during the surge.
# Even one success while wp_auth_failures_total is at 1000+/min is a credible breach.
Each of these queries surfaces wp_auth_attempts_total (or its failure variant), and each of them maps directly to a screenshot in §6.
5.2 Root causes — the four shapes
Each shape produces a distinct fingerprint in wp_auth_attempts_total. Each maps to a different remediation.
Shape A — One IP, many usernames (password spraying).
- Fingerprint: 1–10 source IPs, hundreds-to-thousands of distinct usernames, low password variety per username.
- Signal:
wp_auth_attempts_total{result="failure"}rate spike withsource_ipcardinality near 1,usernamecardinality high. - Log shape: same IP cycling through
admin,administrator,editor,support,info,wpadmin, common first names. Each username tried 1–3 times. - Why: attacker is checking which usernames exist by spraying a single common password (
Password1!,Welcome2024) against a list. WordPress historically leaks username existence via login-error messages and?author=Nenumeration.
Shape B — Many IPs, one username (targeted compromise).
- Fingerprint: 50–500+ source IPs, 1–3 usernames, dozens to hundreds of password attempts per username.
- Signal:
wp_auth_attempts_total{result="failure"}withusernamecardinality near 1,source_ipcardinality high, often spread acrossip_countryvalues. - Log shape: the username is real and known —
[email protected], the founder's handle, an editor whose email is on the About page. - Why: someone is targeting a specific account. They know the username, they have a password list (often from a leaked dump), and they're using a botnet or proxy network to evade per-IP rate limits.
Shape C — Many IPs, many usernames, low rate per pair (distributed credential stuffing).
- Fingerprint: thousands of source IPs, thousands of usernames, exactly 1–2 attempts per (IP, username) pair.
- Signal:
wp_auth_attempts_total{result="failure"}rate climbs steadily,source_ip×usernamenear-uniform, no top of the table.wp_request_fingerprint_topshows scattered fingerprints. - Log shape: each row is unique. No IP appears twice. No username appears twice. The total is huge.
- Why: attacker has a credential dump (email + password pairs from another breach) and is testing each pair against your site exactly once, from a different residential proxy. This is the most dangerous shape — every successful login is a real account takeover with the user's actual password.
Shape D — One IP, one username, sustained low rate (operator lockout).
- Fingerprint: single source IP (often your office), single username, 5–50 attempts over 10–60 minutes.
- Signal:
wp_auth_attempts_total{result="failure"}with both cardinalities pinned at 1,ip_countrymatches the entity's known operator country. - Log shape: same IP, same username, attempts spaced 30s–2min apart (a human typing).
- Why: the username's owner forgot their password and is brute-forcing their own memory. Common, harmless, frequently misclassified as an attack.
5.3 Fix — match the response to the shape
The wrong response to the wrong shape is worse than no response.
Shape A (password spraying). Block the source IP at the edge (Cloudflare WAF rule, nginx deny, fail2ban). Disable ?author=N enumeration and the REST /wp/v2/users endpoint for unauthenticated requests. Rename the default admin account if it still exists. Time to mitigate: minutes.
Shape B (targeted compromise). Do not block IPs — there are too many and the attacker will rotate. Instead: force-rotate the targeted account's password, enable 2FA on that account immediately (and ideally on every privileged account), and audit recent successful logins for that username in the last 30 days for any you don't recognize. Consider that the attacker may already have valid credentials and is testing them; check wp_auth_attempts_total{result="success", username=" for the surge window. Time to mitigate: 30 minutes plus 2FA enrollment.
Shape C (distributed credential stuffing). IP blocking is useless. The defenses that work: enforce 2FA platform-wide, deploy a CAPTCHA on the login form (only for unrecognized devices to keep UX), and subscribe to a credential-leak feed (HaveIBeenPwned, etc.) so you can force-reset users whose emails appear in dumps. Look for the success label specifically — wp_auth_attempts_total{result="success"} rising during the surge means real takeovers. Time to mitigate: hours, plus a policy change.
Shape D (operator lockout). Send the user a password-reset link. Do not ban the IP. Optionally, surface "your account is being locked out" in a Slack channel the user actually reads.
5.4 Verify — what to expect after each fix
Verification is shape-specific, but every shape verifies against the same primary signal: wp_auth_attempts_total.
- Shape A: after the IP block, expect
wp_auth_attempts_total{result="failure", source_ip="to drop to zero within 60 seconds. Healthy baseline for a typical small-to-medium WordPress site is 5–30 failures/hour total — broad scanner noise. If you're still seeing 500+/hour from the same IP after the block, the block didn't apply (check WAF rule order)."} - Shape B: after the password rotation and 2FA, expect
wp_auth_attempts_total{result="success", username="to stay flat (no new successes from the attacker), while"} failuremay continue at low rate as the attacker burns through the rest of their password list. Healthy baseline: 0–2 successes/hour for the target account during business hours, 0 at night. If you see a newsuccessrow from an unfamiliarsource_ip, the attacker had a working credential and you need to invalidate the session. - Shape C: after platform-wide 2FA, expect
wp_auth_attempts_total{result="success"}rate to drop to near zero even whilefailurerate stays high (attackers keep testing, but no one gets in). Healthy baseline: success rate stable at the legitimate-login rate, ~1–5 per hour for a small site, the rest is failure noise. The dangerous signal is any sustainedsuccessrate above the legitimate baseline. - Shape D: after the reset link, expect a single
wp_auth_attempts_total{result="success"}from the operator's IP within 5 minutes, then quiet. Healthy baseline: same operator typically logs in 2–5 times per workday with zero failures.
In all four cases, the verifier is the same signal and the same dashboard panel. The shape of the post-fix curve tells you whether you addressed the right attack class.
6. How to Catch This Early
Fixing it is straightforward once you know the cause. The hard part is knowing it happened at all — and knowing which of the four shapes you're in before the on-call engineer guesses wrong.
This issue surfaces as wp_auth_attempts_total.
Everything you just did manually — pull the auth log, group by IP, group by username, cross-tab the cardinality, watch the success ratio — Logystera does automatically. The same wp_auth_attempts_total you just grepped is detected, charted, and shape-classified by the wp_auth_attempt_surge insight in real time. The insight reads three derived numbers (rate-of-change, success ratio, IP × username cardinality cross), pattern-matches against the four shapes, and labels the alert accordingly. You don't get "1,200 failed logins" — you get "Shape C: distributed credential stuffing, 4,200 IPs, 3,800 usernames, 0.04% success rate, 18 successful logins not in baseline."
!Logystera dashboard — wp_auth_attempts_total over time wp_auth_attempts_total last 24h, broken down by result and source_ip cardinality — the surge starts at 14:03 immediately after a credential dump was posted on a public paste site, with the cardinality cross showing Shape C (distributed stuffing).
!Logystera alert — distributed credential stuffing detected Critical alert fires within 60s of the wp_auth_attempts_total surge, naming the shape (Shape C: distributed credential stuffing), the IP/username cardinality, and the success-ratio anomaly that triggered it.
The fix is simple once you know the problem. The hard part is knowing it happened at all, and knowing which attack you're in. Logystera turns a generic "lots of failed logins" notification into a labeled, actionable shape — Shape A through D, with the specific countermeasure named in the alert body — within 60 seconds of the surge starting.
7. Related Silent Failures
These guides share signal proximity to wp_auth_attempts_total and the broader auth.attempt cluster. If you saw this surge, you'll likely see one of these next:
auth.attempt→ REST API credential stuffing (Tier 1, guide 10): the endpoint-level view of Shape C, focused on/wp-json/jwt-auth/v1/tokenand/xmlrpc.phptraffic.wp_logged_in_requests_total→ logged-in ratio anomaly (Tier 4, guide 12): what happens after a successful credential stuff — the post-auth traffic shape that exposes account takeover.wp.state_change→ admin user added (Tier 1, guide 09): the post-breach signal when Shape B or Shape C succeeded and the attacker created a persistence account.wp_auth_attempts_total→ unexpected logout / session hijacking (Tier 2, guide 16): when forged cookies bypass the auth path entirely and you see logged-in traffic with no matchingwp_auth_attempts_total{result="success"}.wp_request_fingerprint_top→ bot fingerprinting (Tier 4, guide 05): the request-shape view that complements the auth-shape view, useful for confirming Shape A vs Shape C when the IP/username cross is ambiguous.
See what's actually happening in your WordPress system
Connect your site. Logystera starts monitoring within minutes.