2026年11月13日
2026年11月13日
Automatically Delete WordPress Users Who Have Not Logged In (WP-Cron)
This article is a localized edition of the Japanese original. Read the Japanese original
WordPress has no feature that automatically deletes unused users after a set period. Core also does not store "when the user last logged in". So to delete users automatically, you set up your own mechanism that records the time at every login, and uses WP-Cron to find and delete users with an old record once a day.
The code in this article was checked on a WordPress 6.9 test environment in November 2026: only subscribers with an old recorded time were deleted, administrators, other roles and users who logged in recently were kept, and the deleted users' posts were reassigned to the specified administrator.
What to decide before deleting
Deleting users cannot be undone. Before adding the code, decide the following.
| What to decide | Examples and how to think about it |
|---|---|
| Target role | Only "Subscriber", which grows with member registration. Do not target administrators, editors or authors |
| Period | For example, 365 days since the last login. Match the retention period written in your membership terms or privacy policy |
| Handling of posts | Reassign the deleted user's posts to a specified user. If you do not specify who inherits them, the posts are deleted too |
| Advance notice | If you notify users before deletion, build that mechanism separately (the code in this article does not notify) |
| Backups | Back up the database before installing, and again before switching from the dry run to real deletion |
What each role can do is explained in The list of WordPress user roles, and reassigning content on deletion in Reassigning content to another user when deleting a WordPress user.
Step 1: Put the code in mu-plugins
So that it keeps working even if you change themes, put it in the wp-content/mu-plugins/ folder (mu-plugins is the place for plugins that are always loaded without being activated in the admin screen; create the folder if it does not exist). Name the file delete-inactive-users.php.
<?php
/**
* Plugin Name: Delete inactive users
* Description: Deletes subscribers daily once a set number of days have passed since their last login.
*/
defined( 'ABSPATH' ) || exit;
// Settings: days without login before deletion, target roles, who inherits posts, and whether this is a dry run
const MYSITE_INACTIVE_DAYS = 365;
const MYSITE_INACTIVE_ROLES = array( 'subscriber' );
const MYSITE_REASSIGN_TO = 1; // User ID of the administrator who inherits the deleted users' posts
const MYSITE_INACTIVE_DRY_RUN = true; // While true, nothing is deleted; targets are only written to the log
const MYSITE_INACTIVE_BATCH = 50; // Number of users processed per run
// 1. Record the login time
add_action( 'wp_login', 'mysite_record_last_login', 10, 2 );
function mysite_record_last_login( $user_login, $user ) {
update_user_meta( $user->ID, 'mysite_last_login', time() );
}
// 2. Register a once-a-day schedule
add_action( 'init', 'mysite_schedule_inactive_cleanup' );
function mysite_schedule_inactive_cleanup() {
if ( ! wp_next_scheduled( 'mysite_delete_inactive_users' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'mysite_delete_inactive_users' );
}
}
// 3. Delete expired users
add_action( 'mysite_delete_inactive_users', 'mysite_delete_inactive_users' );
function mysite_delete_inactive_users() {
require_once ABSPATH . 'wp-admin/includes/user.php'; // wp_delete_user() lives here
// Users with no record yet start counting from today (prevents everyone from being targeted right after installation)
$no_record = get_users(
array(
'role__in' => MYSITE_INACTIVE_ROLES,
'fields' => 'ID',
'number' => 500,
'meta_query' => array(
array(
'key' => 'mysite_last_login',
'compare' => 'NOT EXISTS',
),
),
)
);
foreach ( $no_record as $user_id ) {
update_user_meta( (int) $user_id, 'mysite_last_login', time() );
}
$limit = time() - MYSITE_INACTIVE_DAYS * DAY_IN_SECONDS;
$expired = get_users(
array(
'role__in' => MYSITE_INACTIVE_ROLES,
'fields' => 'ID',
'number' => MYSITE_INACTIVE_BATCH,
'meta_query' => array(
array(
'key' => 'mysite_last_login',
'value' => $limit,
'compare' => '<',
'type' => 'NUMERIC',
),
),
)
);
foreach ( $expired as $user_id ) {
$user_id = (int) $user_id;
if ( user_can( $user_id, 'manage_options' ) ) {
continue; // Skip administrators, just in case
}
if ( MYSITE_INACTIVE_DRY_RUN ) {
error_log( 'inactive user (dry run): ' . $user_id );
continue;
}
wp_delete_user( $user_id, MYSITE_REASSIGN_TO );
error_log( 'inactive user deleted: ' . $user_id );
}
}
The key points of the code are as follows.
- Recording the last login: the
wp_loginaction runs when a login succeeds. At that point the current time is saved to the user metamysite_last_login. - Users with no record: when you add the code, nobody has a record yet. If users without a record were treated as "expired", everyone would be deleted right after installation, so users without a record start counting from that day. In other words, nobody is deleted until
MYSITE_INACTIVE_DAYSdays have passed since installation. - Deletion:
wp_delete_user()is a function in an admin-screen file (wp-admin/includes/user.php), so it is loaded before being called from WP-Cron. The deleted user's posts and links are reassigned to the user ID given as the second argument. - Users per run: deleting a large number at once makes the process heavy, so it is limited to 50 per day. The rest are processed on following days.
Change the 1 in MYSITE_REASSIGN_TO to the user ID of an existing administrator. You can find a user ID in "Users" → "All Users" by hovering over a name and looking at the link (the number after user_id=).
Step 2: Check the targets with a dry run
At first, leave MYSITE_INACTIVE_DRY_RUN as true so that nothing is deleted and only the targets are written to the log. To send the log to wp-content/debug.log, add these three lines in wp-config.php above require_once ABSPATH . 'wp-settings.php';.
<?php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false ); // Do not show errors on screen
If WP-CLI is available, you can check the schedule and run it once on the spot.
# Check that the schedule is registered
wp cron event list --fields=hook,next_run_relative,recurrence | grep mysite
# Run it once right now
wp cron event run mysite_delete_inactive_users
# Check the log
tail -n 20 wp-content/debug.log
Check that the user IDs in the log are the ones you expected by comparing them with the "Users" list. Right after installation everyone's record is "today", so there are 0 targets. If you want to check the behavior sooner, make MYSITE_INACTIVE_DAYS smaller on a test environment, or set an old time for a test user.
# On a test environment, set the last login of a test user (ID 25) to 400 days ago
wp user meta update 25 mysite_last_login $(( $(date +%s) - 400*86400 ))
When you have finished checking, change MYSITE_INACTIVE_DRY_RUN to false and restore WP_DEBUG in wp-config.php.
Step 3: Run WP-Cron at a fixed time
WP-Cron runs scheduled tasks whose time has passed when someone visits the site. On low-traffic sites, runs can be delayed. To run it at a fixed time every day, set DISABLE_WP_CRON to true in wp-config.php and switch to calling wp-cron.php regularly from the server's cron. For the steps, see How to optimize WP-Cron in production. If scheduled tasks do not run, you can check the cause in What to do when WordPress cron jobs do not run.
Deleting by hand in bulk instead of automatically
If you prefer to review and delete periodically yourself, you can list and delete users with WP-CLI.
# List subscribers, oldest registration first
wp user list --role=subscriber --fields=ID,user_login,user_registered --orderby=user_registered --order=ASC
# Delete IDs 12 and 34 and reassign their posts to user ID 1
wp user delete 12 34 --reassign=1
To delete in the admin screen, check the targets in the "Users" list and choose "Delete" from the bulk actions. When you delete users who have posts, a screen appears asking whether to delete the posts or attribute them to another user.
Cautions
- Multisite: on Multisite,
wp_delete_user()only removes the user from that site; the user is not deleted from the network. To delete from the whole network, usewpmu_delete_user(). The code in this article is for single sites. - Differences by login method: with mechanisms that do not go through the normal WordPress login process, such as social login,
wp_loginmay not run, and the record is not updated even though the user logs in. Check whether your login plugin runswp_loginafter login. - When removing the code: deleting the file alone leaves the registered schedule in the database. After deleting the file, also remove the schedule with
wp cron event delete mysite_delete_inactive_users.
Related articles
- The list of WordPress user roles: what each can do and how to change them
- Reassigning content to another user when deleting a WordPress user
- How to optimize WP-Cron in production (server cron, DISABLE_WP_CRON)
References: wp_delete_user() (WordPress Developer Resources), wp_schedule_event(), The wp_login hook, wp cron event (WP-CLI)
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)