Fixing and monitoring WordPress cron (WP-Cron)
WordPress has its own scheduler, WP-Cron. It publishes scheduled posts, sends WooCommerce emails, runs backup and SEO plugins and checks for updates. It isn’t a real cron, though: it runs only when someone opens the site. That is why scheduled posts show “Missed schedule” and backup plugins skip nights. The fix is to switch it to the system cron and watch that it keeps running.
Why WP-Cron misses runs
- Little traffic. No visitors at 3 AM means no 3 AM run. The task waits for the next page view.
- Page caching. When a CDN or a caching plugin serves the page without starting PHP, WordPress never gets the chance to check its schedule, even with plenty of visitors.
- Blocked loopback. WP-Cron starts by requesting
wp-cron.phpon its own site. A firewall, basic auth on staging or a broken DNS record makes that request fail. Site Health in the admin reports it as a loopback error. - Long tasks on a visitor’s request. Heavy jobs run inside web requests and hit PHP time limits.
Switch off the built-in trigger
Add this line to wp-config.php above /* That's all, stop editing! */:
define( 'DISABLE_WP_CRON', true );
This only stops page views from starting WP-Cron. The scheduled events stay, and from now on the system cron runs them.
Run it from the system cron
With WP-CLI on the server, run due events every five minutes as the user that owns the site files:
# crontab -e for the site's user, e.g. www-data
*/5 * * * * cd /var/www/example.com && /usr/local/bin/wp cron event run --due-now --quiet
Without WP-CLI, for example on shared hosting, request wp-cron.php with curl instead:
*/5 * * * * curl -fsS -m 60 -o /dev/null "https://example.com/wp-cron.php?doing_wp_cron"
WP-CLI is the better option. It doesn’t depend on the web server, it isn’t limited by the request timeout, and its exit code tells you whether it worked.
See what is scheduled
wp cron event list # every event, its hook and the next run wp cron schedule list # available intervals wp cron event run woocommerce_cleanup_sessions # run one event now
In the admin, the free WP Crontrol plugin shows the same list. Events with a next run in the past mean the scheduler isn’t running.
WooCommerce and many plugins queue work in Action Scheduler (Tools → Scheduled Actions), which runs on top of WP-Cron. If WP-Cron stops, that queue stops too, along with order emails, subscription renewals and webhooks.
Get an alert when it stops
Now the system cron runs WordPress’s schedule, so it’s the system cron that needs watching. Create a check in Heartcron with a 5-minute period and a grace period of, say, 10 minutes, and add a ping to the line:
*/5 * * * * cd /var/www/example.com && /usr/local/bin/wp cron event run --due-now --quiet; curl -fsS -m 10 --retry 3 -o /dev/null https://ping.heartcron.com/<uuid>/$?
The ping reports WP-CLI’s exit code: 0 marks the check up, anything else sends an alert right away. If the crontab line disappears, the server is moved or PHP breaks after an update, no ping arrives and you get an alert after the grace period. Run the command once by hand and check that it exits with 0, including when no events are due: wp cron event run --due-now; echo $?.
Watch the jobs that matter on their own
One check tells you the scheduler runs. For a job you’d really miss, such as a nightly backup plugin or a daily export, add a separate check and ping it from the job itself. In a plugin or a theme’s functions.php:
add_action( 'my_nightly_export', function () {
$url = 'https://ping.heartcron.com/<uuid>';
wp_remote_get( $url . '/start', [ 'timeout' => 10 ] );
try {
my_run_export();
wp_remote_get( $url, [ 'timeout' => 10 ] );
} catch ( Throwable $e ) {
wp_remote_post( $url . '/fail', [ 'timeout' => 10, 'body' => $e->getMessage() ] );
throw $e;
}
} );
Give that check the same schedule as the event. Not sure how to write it as cron? The cron expression explainer shows the next runs.