Guide
WordPress hundreds of new users overnight — detecting bulk spam registration before they pollute your database
1. Problem
You opened wp-admin/users.php this morning and the user count went from 247 to 2,113 overnight. The most recent page is filled with rows you don't recognize: random alphanumeric usernames, Gmail and ProtonMail addresses with + aliases, registration timestamps clustered between 03:00 and 05:30 UTC, and "Subscriber" role on every one.
This is the textbook "wordpress hundreds of new users overnight" scenario. It surfaces in Logystera as a vertical wall on wp_user_lifecycle_total with action="created" — a 10× spike against a baseline of 2–5 registrations per day. By the time you see it in wp-admin, the database already contains 1,800+ rows of fake-but-real-looking PII you did not consent to store: email addresses, IPs in usermeta, and whatever fields your registration form captured.
The standard advice — "install a CAPTCHA" — is correct prevention but doesn't tell you which 1,866 users to delete, whether any of them logged in, or whether your wp_users table got merged with a botnet's email list before you noticed.
2. Impact
The visible damage is a polluted user table. The real damage is wider:
- GDPR / privacy exposure. Spam accounts contain real-looking email addresses, IP addresses recorded by registration plugins, and any custom profile fields. You did not consent to store 1,866 botnet-supplied addresses, and "we got spammed" is not a lawful basis under GDPR Article 6. A subject access request from any of those addresses (some are real people whose addresses were re-used) becomes a 30-day legal clock.
- Email reputation. If your registration flow sends a welcome or confirmation email, your transactional sender just blasted 1,866 unconfirmed addresses in one window. SendGrid, Postmark, and SES all watch the bounce-and-spam-complaint ratio over a rolling window — one night of this can put your sender on a soft suspension that breaks legitimate password resets for a week.
- Outbound spam vector. If the Subscriber role allows commenting or BuddyPress profile creation, the bots now have 1,866 footholds to drop comment spam and farm backlinks. Your domain authority drops; Akismet starts flagging real users.
- Database bloat.
wp_users,wp_usermeta, and any membership plugin's join tables grow by 5–10× their normal row count. Query plans that were fine yesterday are suddenly slow today, and the admin user list gets visibly laggy.
A single overnight run is recoverable. A week of unnoticed slow drip — 50–100 fakes a day — is not, because by then real users have registered with adjacent IDs and bulk-delete-by-date no longer works cleanly.
3. Why It’s Hard to Spot
WordPress's default user-creation pipeline is silent on purpose. wp_create_user() succeeds, user_register fires, wp_new_user_notification() queues the welcome mail, and the request returns 200. There is no error, no admin email by default (the "send admin notification on new user" setting is off in most setups), and wp-admin/users.php doesn't sort by registration timestamp on the default view — it sorts by username.
Standard monitoring misses bulk registration for compounding reasons:
- Uptime monitors are blind. The site is up.
/wp-login.php?action=registerreturns 200. Pingdom sees green. - Access logs look normal. Each registration is a single POST. 1,866 requests over four hours is roughly 8/minute — well under any rate-limit threshold.
- No
auth.attemptfailure spike. Bulk registration is not a brute-force login; the bot is succeeding at the registration endpoint, by design.wp_auth_attempts_totalstays flat. Wordfence, iThemes Security, and Limit Login Attempts all sit silent because nothing failed. - The
createdsignal looks like growth. A 10× spike in user creation is indistinguishable from a successful marketing campaign — until you check the actor label.
The result is a silent failure: by the time someone scrolls the user list, the data is in the database, the welcome emails are in the queue, and your sender reputation is already moving.
4. Cause
Every time WordPress creates, updates, or deletes a user, the Logystera WP plugin emits wp_user_lifecycle_total with an action label (created/updated/deleted), an actor label (anonymous/self/admin), and a role label.
Two distinct creation patterns produce identical-looking rows in wp_users:
- Legitimate growth.
wp_user_lifecycle_total{action="created", actor="admin"}— a logged-in admin created the user viawp-admin/user-new.php. Oractor="self"paced over hours with realistic email distributions. - Bulk spam.
wp_user_lifecycle_total{action="created", actor="anonymous"}— the registration endpoint is open and the request had no logged-in session. Pacing is 5–20/minute. The supporting signal iswp_emails_total{event="enqueued", template="user_register"}rising in lockstep, because every successfulwp_create_user()queues a confirmation mail.
The plugin hooks user_register, profile_update, and delete_user, captures the actor from the current request context, and emits the lifecycle signal with actor and source URI attached. Because the signal is emitted from inside the WP request, the actor label is the real actor — not an inference — and that label is how legitimate growth is mechanically separated from spam.
5. Solution
5.1 Diagnose (logs first)
Diagnosis is mechanical: confirm the spike, separate actor=anonymous from actor=admin, correlate with wp_emails_total, and time-bisect against the registration form's deploy.
1. Confirm the spike from the WP database directly.
wp_users.user_registered is the source of truth. Run this from the host shell — wp here is wp-cli:
# How many users registered in the last 12h vs the 7-day average?
wp db query "SELECT DATE(user_registered) AS day, COUNT(*) AS n
FROM wp_users
WHERE user_registered > DATE_SUB(NOW(), INTERVAL 14 DAY)
GROUP BY day ORDER BY day DESC LIMIT 14;"
A healthy site shows a flat 2–10/day baseline with mild weekly seasonality. A spam burst shows a single row at 1,800+, often clustered in 4–6 hours overnight. This is exactly what wp_user_lifecycle_total{action="created"} is measuring; you're confirming the metric against the underlying table.
2. Separate spam pattern from legitimate growth — check the actor.
The wp_user_lifecycle_total signal carries an actor label. From wp-cli, you can reproduce the same split against the database to confirm what the metric reported:
# Spam bursts come from the registration endpoint (anonymous), not admin
wp db query "SELECT user_login, user_email, user_registered, user_status
FROM wp_users
WHERE user_registered > DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY user_registered;" | head -n 50
What you're looking for, the email pattern is the smoking gun:
- Real users:
[email protected],[email protected]— varied domains, varied formats. - Spam bots:
[email protected],[email protected], plus+tag-aliased Gmail addresses on the same root mailbox. Local-part is often 5–8 random alphanumeric characters.
This is the diagnostic that maps to wp_user_lifecycle_total{action="created", actor="anonymous"} — every row above corresponds to one of those metric increments.
3. Cross-reference wp_auth_attempts_total to rule out compromise.
wp db query "SELECT u.user_login, u.user_registered
FROM wp_users u
JOIN wp_usermeta s ON s.user_id = u.ID AND s.meta_key = 'session_tokens'
WHERE u.user_registered > DATE_SUB(NOW(), INTERVAL 24 HOUR);"
If wp_auth_attempts_total{result="success"} for any of those usernames is non-zero, one of the bot accounts authenticated — that changes the response from "delete the rows" to "rotate any secrets the account could have touched."
4. Confirm the email queue blew up.
grep -E "wp_mail.*user_register|wp_new_user_notification" /var/log/php-fpm/error.log \
| awk '{print $1, $2}' | sort | uniq -c | sort -rn | head -n 10
This correlates wp_emails_total{event="enqueued", template="user_register"} with the bulk-registration window. If the count matches the user-creation count within ~5%, every bot got a welcome email — which is exactly the email-reputation problem.
5. Time-correlate with the most recent registration-form change.
wp_user_lifecycle_total spikes don't happen in a vacuum. They come after a real-world change: registration was re-enabled, a CAPTCHA plugin was deactivated, a membership plugin was installed, or users_can_register was flipped to 1.
# Did anything touch options or activate plugins recently?
wp option get users_can_register
wp option get default_role
wp plugin list --status=active --format=csv
# Cross-reference with wp_state_changes_total: when was users_can_register flipped?
grep -i "users_can_register\|default_role\|recaptcha\|akismet" \
/var/log/php-fpm/error.log | tail -n 20
This is the time-correlation step: if wp_state_changes_total{option="users_can_register", new_value="1"} fired at 02:48 UTC and the first wp_user_lifecycle_total{actor="anonymous"} fired at 03:14 UTC, you have your story — registration was re-enabled (possibly by a deploy, possibly by a hijacked admin), and bots found it within 26 minutes.
5.2 Root Causes
Each cause maps to a specific signal pattern.
- Registration left open with no CAPTCHA.
users_can_register=1and the form has no challenge. Produceswp_user_lifecycle_total{action="created", actor="anonymous"}at 5–20/minute, paired withwp_emails_total{event="enqueued", template="user_register"}at the same rate. The most common cause. - Membership plugin re-enabled registration silently. Activating MemberPress, Paid Memberships Pro, or BuddyPress flips
users_can_register=1as a side effect. Produceswp_state_changes_total{option="users_can_register"}followed by the lifecycle spike within minutes. - REST
/wp/v2/usersPOST endpoint open. A custom plugin or theme exposed user creation via REST without auth. Produceswp_user_lifecycle_total{action="created", actor="anonymous", source="rest"}andhttp_requests_total{path="/wp-json/wp/v2/users", method="POST", status="201"}. default_roleset to something privileged. Ifdefault_role=authoror higher, every spam registration becomes a content-publishing footprint. Produceswp_user_lifecycle_total{action="created", role="author"}— therolelabel is the smoking gun.- Compromised admin creating users programmatically. Rare but high-severity. Produces
wp_user_lifecycle_total{action="created", actor="admin"}at unusual rates, often paired with priorwp_auth_attempts_total{result="success"}from a foreign IP. Cleanup below does not apply — treat as a security incident.
5.3 Fix
Match the fix to what the actor label told you.
Step 1 — stop the bleeding. Disable open registration immediately, before doing anything else. Every minute you spend cleaning up is another 8–20 spam rows.
wp option update users_can_register 0
wp cache flush
If a membership plugin needs registration on, scope it: enable registration on a specific page/role and require email confirmation or CAPTCHA before wp_create_user() runs.
Step 2 — identify the spam window with wp-cli. Use the timestamps from the diagnose step. Be conservative — pick a window that starts after the last known-good registration:
# Dry-run: list the candidates first
wp user list --field=ID --role=subscriber --registered_after="2026-04-26 23:00:00" \
--registered_before="2026-04-27 06:00:00"
Step 3 — bulk-delete with reassignment. Never use raw DELETE FROM wp_users — it skips the delete_user hook, leaves orphaned wp_usermeta rows, and skips your audit trail. Use wp-cli, which fires the proper hooks (and produces wp_user_lifecycle_total{action="deleted"} so the cleanup is itself audited):
# Re-run wp user list, pipe into wp user delete with --reassign=0 (no content reattribution needed for subscribers)
wp user list --field=ID --role=subscriber \
--registered_after="2026-04-26 23:00:00" \
--registered_before="2026-04-27 06:00:00" \
| xargs -n 1 wp user delete --yes --reassign=1
--reassign=1 reassigns any orphaned content to user ID 1 (your admin). For pure-subscriber spam this is rarely needed, but it's the safe default.
Step 4 — purge orphaned meta and queued emails.
wp db query "DELETE m FROM wp_usermeta m
LEFT JOIN wp_users u ON m.user_id = u.ID
WHERE u.ID IS NULL;"
If you use a transactional email plugin with a queue (FluentSMTP, WP Mail SMTP Pro), purge unsent welcome mails before they ship.
Step 5 — install the prevention layers. Defense in depth:
- CAPTCHA on the registration form. hCaptcha, Cloudflare Turnstile, or reCAPTCHA v3 — blocks 95%+ of bots.
- Email confirmation gate. Require
email_confirmbefore the row goes live. Plugins: New User Approve, BuddyPress's built-in confirmation. - Akismet for users. The
akismet_check_userfilter; same heuristics that catch comment spam catch registration spam. - Email allowlist / denylist at the form layer via
pre_user_email. Block+-aliased Gmail and high-abuse domains. - Rate-limit the registration endpoint at the web server:
limit_reqin nginx scoped to/wp-login.php?action=register, 1 req/30s per IP.
5.4 Verify
You're looking for two things to hold simultaneously: wp_user_lifecycle_total{action="created", actor="anonymous"} returns to baseline, and wp_emails_total{template="user_register"} rate matches it.
wp db query "SELECT COUNT(*) FROM wp_users
WHERE user_registered > DATE_SUB(NOW(), INTERVAL 1 HOUR);"
Healthy state for wp_user_lifecycle_total on a site with open registration: 2–10 events per day with actor="anonymous", ~95% paired with a wp_emails_total{event="delivered"} within 60 seconds. If the rate stays at zero for an hour after closing registration, cleanup is complete. Anything above 1/minute sustained for 5 minutes is anomalous; anything above 5/minute is bulk spam.
If wp_user_lifecycle_total settles but wp_emails_total{event="bounced"} is still elevated, you closed the front door but still have sender-reputation cleanup to do with your transactional provider. If wp_user_lifecycle_total{actor="anonymous"} reappears within an hour at the spam rate, the CAPTCHA isn't engaging — switch to Turnstile or hCaptcha.
6. How to Catch This Early
Fixing it is straightforward once you know the cause. The hard part is knowing it happened at all.
This issue surfaces as wp_user_lifecycle_total.
Everything you just did manually — query wp_users for the last 24h, group by registration timestamp, separate actor=anonymous from actor=admin, correlate with wp_emails_total to confirm the welcome-mail blast — Logystera does automatically. The WP plugin hooks user_register, captures the actor from the current request context, and emits wp_user_lifecycle_total with action, actor, role, and source labels in real time. The same labels you just split on in wp-cli are the labels Logystera charts.
!Logystera dashboard — wp_user_lifecycle_total over time wp_user_lifecycle_total{action="created"} rate, last 24h — vertical wall at 03:14 UTC, 1,866 anonymous registrations, immediately after users_can_register was flipped on at 02:48 UTC.
The rule that fires is id 814 — bulk anonymous registration burst, severity high, threshold >20 events with actor=anonymous in 5 minutes (configurable per entity). The supporting condition cross-references wp_auth_attempts_total{result="success"} from the same usernames within the next hour and escalates to critical if any of the new accounts authenticate — the path from "spam" to "potential compromise."
!Logystera alert — bulk anonymous registration burst High alert fires within 60s of the threshold breach, including the actor label, sample usernames, and the supporting wp_emails_total enqueue rate.
The alert payload includes the timestamp, the affected entity, the actor label (anonymous vs admin — the difference between "spam cleanup" and "incident response"), the registration rate per minute, the most recent wp_state_changes_total event for users_can_register, and a sample of the new usernames. That's enough to decide whether to run the cleanup in §5.3 or escalate to an incident, from the alert body alone.
The fix is simple once you know the problem. The hard part is knowing it happened at all. Logystera turns this from a "discover it tomorrow when you scroll the user list" emergency into a 60-second notification — early enough to close registration and stop the welcome-email blast before your sender reputation moves.
7. Related Silent Failures
wp_emails_total{event="bounced"}spike — email-side companion (T6 #11). Bulk registrations enqueue welcome mails to invalid addresses; bounces come back over 6–24h and tank your sender score.wp_auth_attempts_total{result="success"}from new accounts — the escalation path. If any spam-registered account logs in, the incident class changes from "spam cleanup" to "compromise."wp_state_changes_total{option="users_can_register"}— the upstream cause. A change here precedes the lifecycle spike by minutes; alerting on the option flip catches the problem before any spam row exists.http_requests_total{path="/wp-json/wp/v2/users", method="POST"}— the REST variant. If user creation is exposed via REST, the lifecycle spike is paired with a request spike on this path.wp_user_lifecycle_total{action="updated", actor="anonymous"}— rarer but worse. Indicates an open profile-update endpoint, letting bots modify existing users (display name, URL field for backlink farming).
See what's actually happening in your WordPress system
Connect your site. Logystera starts monitoring within minutes.