Fixing and monitoring WordPress cron (WP-Cron)

Updated November 3, 2026

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

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.

More guides