Bali, Indonesia
Insights

Reading wp cron event list to Find Orphaned Hooks Left by Deleted Plugins

October 21, 2026
0:00
A site owner says the WordPress dashboard got slower over the past year, even though they keep removing plugins they no longer use. The frontend is cached and fine. The admin is not. One place that often gets missed is the scheduled events list, because deleting a plugin from the Plugins screen does not necessarily remove the cron events it scheduled. Those leftover events are what this article calls orphaned hooks. WordPress still knows when to run them, but nothing is listening when they fire. Below is the order I would check things in, and what each result tells you.
WP-Cron, WordPress's built-in scheduler, stores every scheduled event in a single row of the wp_options table, named cron. The value is a serialized PHP array grouped by timestamp, then by hook name, then by a signature of the event's arguments. Each entry also records the recurrence, such as hourly, twicedaily, or daily. When an event comes due, WordPress fires the hook with do_action. If no plugin has attached a function to that hook, nothing happens and no error is logged. For a recurring event, WordPress then schedules the next occurrence anyway. Rescheduling happens regardless of whether any callback ran, so a recurring event from a deleted plugin repeats indefinitely. A well-behaved plugin clears its events on deactivation or in its uninstall routine. Many plugins don't, or only clear some of them. Plugins removed by deleting their folder over FTP or the file manager never run any cleanup code at all. One orphaned daily event costs almost nothing. The trouble comes from accumulation, especially plugins that schedule events per post, per order, or per user with unique arguments. Each argument set is a separate entry. The cron option is normally autoloaded, which means WordPress loads and unserializes it on every request, admin pages included. A cron option that has grown large adds work to every single page load. The broader picture of autoloaded options slowing the dashboard is covered in Why WordPress Admin Is Slow Even When the Frontend Loads Fast. There is a second effect. If events are constantly overdue, for example because the site has little traffic and relies on visitors to trigger WP-Cron, page loads keep trying to spawn a cron run. Core limits this with a lock (the doing_cron transient and WP_CRON_LOCK_TIMEOUT), but a long overdue list keeps the scheduler busy doing nothing useful.
Start with a plain listing. This command is read-only.
BASH
wp cron event list --fields=hook,next_run_relative,recurrence --format=table
Run it from the WordPress root as the site's own user, not root. On shared hosting that usually means SSH into the account and cd into public_html or wherever WordPress lives. On multisite, add --url= with the specific site's address, because each site in the network keeps its own cron option. There are three things to look for in the output. The first is hook names you don't recognize. Plugins usually prefix their hooks with their own slug or an abbreviation of it. A prefix that matches nothing in your current plugin list is the first candidate. The second is next_run_relative showing now or a time in the past for many events. A few overdue events are normal on a quiet site. A long run of them points to either a broken cron setup or a lot of dead weight. If you suspect the cron trigger itself, How to Verify Your WordPress System Cron Job Is Actually Running walks through that separately. The third is the same hook repeated many times. That usually means per-object scheduling with different arguments. It is the pattern most likely to bloat the option. If the list is long, --format=csv piped through sort and uniq -c makes repeated hooks easier to count. For a quick view, the table is enough.
Recognizing names by eye only goes so far. A more reliable signal is to ask WordPress directly whether anything is attached to each hook. The function has_action() returns false when a hook has no callbacks registered. This one-liner loads WordPress once, walks the cron array, and prints each hook that has no callback, along with a count of how many scheduled entries it has. It only reads data.
BASH
wp eval 'foreach ( _get_cron_array() as ts =>hooks ) { foreach ( hooks ashook => events ) { if ( ! has_action(hook ) ) { echo $hook . PHP_EOL; } } }' | sort | uniq -c | sort -rn
The result is a candidate list, not a delete list. The check can only see callbacks that were registered while WP-CLI booted WordPress, and that context is different from a real cron run or an admin page view. Some plugins attach their cron callbacks only when DOING_CRON is defined, only inside is_admin(), or only after a specific class is loaded. Those hooks look orphaned here but work fine in practice. A few core hooks can show up for the same reason, depending on version and context. Don't add --skip-plugins or --skip-themes to this command. Skipping them hides every callback those plugins register, and almost everything would look orphaned.
For each candidate, the question is whether any code on the server still references that hook name. If nothing does, it is orphaned. If something does, find out what that code is before touching the event. The simplest approach is a recursive text search for the exact hook name across wp-content/plugins, wp-content/mu-plugins, and the active theme. Use grep -r over SSH or the search in your control panel's file manager. There are three likely outcomes. No match anywhere: the code that scheduled the event is gone. This is the typical leftover from a deleted plugin, and it is safe to remove. A match inside an inactive plugin that is still installed: the plugin was deactivated without cleaning up. Decide whether the plugin is coming back. If it isn't, remove the plugin properly first, then clear its events. A match inside an active plugin, theme, or mu-plugin: this is a false positive from the second check. The callback is registered in a context WP-CLI didn't hit. Leave it alone. If the event looks wrong, take it up with that plugin's settings or developer. From the support side, the hooks that most often confuse people are the ones with generic names and no obvious prefix. When a search turns up nothing and the name gives no hint, the database is the only place left to look. A backup or old export from before the plugin was removed sometimes shows which plugin's options sat next to it.
Take a database backup before removing anything, for example with wp db export to a file outside the web root. Removing a cron event can't be undone from the dashboard, and restoring the cron row by hand is unpleasant. WP-CLI provides wp cron event delete followed by the hook name. Depending on your WP-CLI version, it removes every scheduled instance of that hook, or it needs an extra flag to do so. Run wp help cron event delete first to see what your installed version supports. Work one hook at a time and re-run the list after each one, rather than scripting a bulk delete from the candidate list. The false positives from the second check are exactly the kind of mistake a loop will make for you. After cleanup, wait at least a day and list the events again. An event that reappears was not orphaned. Something is still scheduling it, usually on init or admin_init with a wp_next_scheduled() check, and it will keep coming back until you deal with that code. This recheck is the most honest test you have that a hook was truly dead. If the problem was per-object events with thousands of entries, check the size of the cron option after deletion. It should shrink noticeably, and admin pages load less data on every request. If the option is still large, the remaining weight is coming from a plugin that is still active, and that becomes a configuration question rather than cleanup. The practical rule is to treat the cron list like the autoloaded options list: review it whenever plugins are removed, not only when the dashboard slows down. Read the list, check which hooks have no callback, confirm by searching the code, and delete one hook at a time with a backup in hand. Most of the time it's a few minutes of work, and it saves you from slowly accumulating overhead that nobody can explain later.