A WordPress cron job is a task WordPress runs on a schedule: publishing a scheduled post, checking for updates, sending a digest email, or syncing stock. WordPress handles these through WP-Cron, which does not run on a clock. It checks for due tasks when someone loads a page, so on a quiet site, or on a site where every page is served from cache, tasks run late or not at all. The reliable setup is to switch off the page-load trigger with DISABLE_WP_CRON and have the server run WordPress cron every few minutes instead.
How WP-Cron actually runs
WP-Cron keeps a list of scheduled events in the database. Each event is a hook name, a next-run time, an optional recurrence and optional arguments. At the end of a normal page request, WordPress looks at that list. If anything is due, it fires off a quick background request to its own wp-cron.php file and does not wait for the answer, so the visitor's page is not slowed down. That second request runs the due tasks.
Three consequences follow, and they explain nearly every cron problem we are asked to look at.
- No traffic, no cron. The official documentation's own example: schedule a task for 2:00 PM, and if no page loads until 5:00 PM, it runs at 5:00 PM. Tasks are not lost. They wait in the queue until the next opportunity.
- Cached pages do not count. If a page cache, a server cache or a CDN answers the request, PHP never runs, and WordPress never gets the chance to check the list. A site that is well cached can be busy with visitors and still starve its own scheduler.
- The site has to be able to call itself. That background request goes from your server to your own domain. A firewall, a security plugin, basic authentication on staging, or a CDN challenge can block it. Site Health reports this as "Your site could not complete a loopback request".
WP-Cron also schedules by interval, not by clock time. A daily event runs roughly 24 hours after the last run, not at a fixed minute of the day. The built-in schedules are hourly, twice daily, daily and weekly, and plugins can add their own.
Signs your WordPress cron jobs are not running
- Scheduled posts showing "Missed schedule" in the posts list.
- Updates not detected for days, because the update checks themselves are cron events.
- Backups, newsletters or reports from plugins arriving late or in bursts.
- WooCommerce subscription renewals, emails or webhooks lagging.
- Site Health under Tools showing "A scheduled event is late" or "A scheduled event has failed".
To see the real state, list the events with WP-CLI. Anything with a next run time in the past is overdue:
# All scheduled events: hook, next run (UTC and relative) and recurrence
wp cron event list
# Add the arguments and schedule name
wp cron event list --fields=hook,next_run_relative,recurrence,schedule,args
# Check the spawning system end to end (reports if DISABLE_WP_CRON is set)
wp cron test
Without shell access, the free WP Crontrol plugin shows the same list under Tools, then Cron Events, including which function each event calls. It warns about events with no attached function and events that have missed their schedule, and it lets you run an event immediately, which is the quickest way to tell "never triggered" from "triggered and failed".
Disable WP-Cron and set up a real cron job
This is the fix for most sites with real traffic or real caching, and it is what the official WordPress plugin handbook recommends when tasks must run on time. It takes two changes, both described in the plugin handbook's guide to hooking WP-Cron into the system task scheduler.
Step 1: stop WordPress triggering cron on page loads
Add this to wp-config.php, above the line that says to stop editing:
// Do not check for due cron events on every page load.
// A server cron job (step 2) runs them instead.
define( 'DISABLE_WP_CRON', true );
This does not switch scheduling off. Events are still stored and wp-cron.php still runs them when it is called. It only stops the check at the end of each visitor's request.
Do step 2 at the same time. DISABLE_WP_CRON without a server cron job means nothing runs at all: no scheduled posts, no update checks, no plugin tasks.
Step 2: run WordPress cron from the server
If your host gives you SSH and WP-CLI, run the due events directly. Edit the crontab of the user that owns the site files, not root, so files created by tasks get the right owner:
# crontab -e (as the site's user)
# Every 5 minutes: run all WordPress cron events that are due
*/5 * * * * cd /var/www/example.com/public && wp cron event run --due-now --quiet
Without WP-CLI, request wp-cron.php over HTTP, which is the method the handbook shows:
# Every 5 minutes: ask WordPress to run due events
*/5 * * * * wget -q --delete-after https://example.com/wp-cron.php
A few practical notes:
- Every five minutes suits most sites. A store with time-sensitive emails or a membership site can use every minute. Overlap is handled:
--due-nowrespects the same lock WordPress uses, so two runs do not process the same events at once. - Use the full path to
wpif the cron environment does not have it on its PATH. Runwhich wpin your shell to find it. - On multisite, every site has its own event list. Loop over the sites:
wp site list --field=url | xargs -I % wp cron event run --due-now --url=% - The HTTP method still needs the loopback to work through your firewall and CDN, and still counts as a web request against your PHP limits. The WP-CLI method avoids both.
- Managed hosts often have a cron panel, or run WordPress cron for you. Check their documentation before adding your own, or you will run everything twice as often as you think.
After the change, run wp cron event list again a few minutes later. The overdue events should be gone and the next-run times should be in the future.
Action Scheduler: the WooCommerce side of WordPress cron
If you run WooCommerce, much of the important background work is not a plain WP-Cron event. WooCommerce uses Action Scheduler, a job queue library that also powers subscription renewals, webhooks and many emails. Action Scheduler stores each job in its own database tables and keeps a log of when it started, finished or failed.
What matters for cron: by default, Action Scheduler is started by WP-Cron. It schedules a WP-Cron event that tries to run every minute, and it also checks for pending jobs at the end of admin page loads. So a store with a broken WP-Cron also has a stalled Action Scheduler queue, which shows up as renewals and emails that bunch up and then all go out at once.
Where to look:
- WooCommerce, then Status, then Scheduled Actions (or Tools, then Scheduled Actions). Filter by Pending and Failed. A failed action shows its error in the log, which usually names the plugin at fault.
- Past-due actions are normal in small numbers, according to the Action Scheduler documentation. Hundreds of them, growing, means the queue is not being run often enough.
- For large queues, run it from the command line. The Action Scheduler WP-CLI documentation recommends WP-CLI for big sites, long-running tasks and sites with heavy cron usage, because web requests are subject to timeouts that a command-line run avoids:
# Process pending Action Scheduler jobs from the command line
wp action-scheduler run
# Remove old completed and canceled actions (defaults: older than 31 days)
wp action-scheduler clean
With DISABLE_WP_CRON and a server cron job in place, Action Scheduler is started reliably along with everything else. Busy stores can add a second crontab line for wp action-scheduler run.
How to register a WordPress cron job in code
If you are building the task yourself, in a custom plugin rather than the theme, the pattern is: add a hook, attach your function to it, schedule the event once, and remove it when the plugin is deactivated. The most common mistake is scheduling on every page load without checking first. Each call adds another copy, and sites end up with the same event scheduled thousands of times.
<?php
/**
* Plugin Name: Example Stock Sync
*/
// 1. Optional custom interval. Intervals are in seconds.
add_filter( 'cron_schedules', function ( $schedules ) {
$schedules['mg_every_fifteen_minutes'] = array(
'interval' => 15 * MINUTE_IN_SECONDS,
'display' => 'Every 15 minutes',
);
return $schedules;
} );
// 2. The function that does the work, attached to our own hook.
add_action( 'mg_sync_stock', 'mg_sync_stock_run' );
function mg_sync_stock_run() {
// Keep each run short. Hand large batches to Action Scheduler.
}
// 3. Schedule once, when the plugin is activated, and only if not already scheduled.
register_activation_hook( __FILE__, function () {
if ( ! wp_next_scheduled( 'mg_sync_stock' ) ) {
wp_schedule_event( time(), 'mg_every_fifteen_minutes', 'mg_sync_stock' );
}
} );
// 4. Remove every scheduled copy when the plugin is deactivated.
register_deactivation_hook( __FILE__, function () {
wp_clear_scheduled_hook( 'mg_sync_stock' );
} );
Two details catch developers out. First, events are matched by hook name and arguments together, so an event scheduled with arguments must be cleared with the same arguments. Second, if a plugin is deleted without being deactivated cleanly, its events stay behind with nothing attached. The WordPress handbook notes that many plugins do not clean up after themselves, and WP Crontrol flags these orphans as events with no action.
For a task that processes hundreds or thousands of items, such as an import or a mass email, use Action Scheduler functions like as_enqueue_async_action() or as_schedule_recurring_action() instead. They run in batches, retry visibly and leave a log, which a single long WP-Cron callback does not. We build background jobs this way in our custom plugin work.
WordPress cron jobs and security
Scheduled tasks are a favorite place for malware to hide, because they run automatically, they are invisible from the normal admin screens, and they survive a partial cleanup.
We saw exactly this in a reinfecting malware cleanup. A group of business sites kept getting reinfected after earlier cleanups. The malware had hidden a "repair" routine inside the configuration file and in scheduled tasks, and whenever part of the infection was removed, it rebuilt the fake plugins, hidden folders and tasks. Removing it piece by piece could never work. After a full backup, we removed every part in a single pass, including the hidden cron jobs, and the sites stayed clean.
What to check if you suspect a compromise:
- Every hook in
wp cron event list, or in WP Crontrol. Most core events start withwp_. For anything else, you should be able to name the plugin, theme or core feature it belongs to. WP Crontrol shows the function each event calls, which tells you where the code lives. - Events with odd arguments, such as long encoded strings or URLs.
- The server's own crontab for the site user (
crontab -l), and the cron panel in your hosting account. Malicious code does not have to use WP-Cron to schedule itself. - Delete events only after the code behind them is gone. If a backdoor is still in place, it will schedule the event again within minutes. Removing the scheduled task is the last step of a cleanup, not the first.
# Remove every scheduled copy of a hook you have confirmed is malicious
# (only after the code that schedules it has been removed)
wp cron event delete suspicious_hook_name
Do not delete core events to tidy up. They handle update checks, cleanup of old data and scheduled publishing, and WordPress expects them to be there. If you are not sure whether a hook belongs to core, look it up in the WordPress code reference before touching it.
Keeping WordPress cron healthy
- Use
DISABLE_WP_CRONwith a server cron job on any site that is cached or that depends on timely tasks. - Check Site Health monthly for late or failed scheduled events and loopback failures.
- On WooCommerce, glance at Scheduled Actions for failed and growing pending counts.
- After removing a plugin, look for its orphaned events.
- After any security incident, review every scheduled event and the server crontab before calling the site clean.
These checks are part of what a WordPress maintenance plan covers, alongside tested updates, daily off-site backups and uptime and security monitoring. If cron is quietly failing, update checks are usually failing with it, which is how a site ends up months behind on security releases without anyone noticing.
Common questions
Why are my scheduled posts showing "Missed schedule"?
Because WP-Cron did not run at the scheduled time. Either no uncached page load happened, the loopback request to wp-cron.php was blocked, or DISABLE_WP_CRON is set with no server cron job behind it. Set up a real cron job as described above and run wp cron test to confirm the rest of the system is working.
Should I disable WP-Cron?
Yes, if you replace it. Disabling the page-load trigger and running cron from the server every few minutes is more reliable and removes the check from visitor requests. Disabling it without a replacement stops every scheduled task on the site.
How often should the server cron job run?
Every five minutes suits most sites. Stores, membership sites and anything sending time-sensitive email can run every minute. Running more often than events are due does no harm, because it simply finds nothing to do.
Is it safe to delete WordPress cron events?
It is safe to delete events left behind by plugins you have removed, and events you have confirmed are malicious once their code is gone. Do not delete core WordPress events (most begin with wp_), and do not delete an active plugin's events unless you know what that plugin uses them for, because it will usually just recreate them.
Does WooCommerce use WP-Cron?
Indirectly. WooCommerce uses Action Scheduler for much of its background work, and Action Scheduler is started by WP-Cron by default, as well as by admin page loads. A store with unreliable WP-Cron will see delayed renewals, webhooks and emails. You can also run the Action Scheduler queue directly with wp action-scheduler run.
What is the difference between WP-Cron and a real cron job?
A real cron job is run by the server's scheduler at fixed times, whether or not anyone visits the site. WP-Cron is a list of tasks inside WordPress that only gets checked when a page loads. The best setup uses both: WordPress keeps the list, and the server's cron makes sure it is checked on time.
Next step
If you would rather not manage crontabs, queues and update checks yourself, our maintenance plans start at $59 a month and keep these background jobs, updates and backups running and monitored. For a custom scheduled integration or background process, start a project and we will send a written scope and a fixed price.
Want a senior developer to look at your site?
Tell us what you need. You get a written scope and a fixed price, usually within one business day.


