
Table of Contents
WP-Cron not working is almost always because it fires only on page visits, so low-traffic sites miss scheduled tasks. The fix is to disable the built-in trigger with DISABLE_WP_CRON in wp-config.php and run wp-cron.php from a real system cron job every five minutes. You know it worked when wp cron event list shows no overdue items and your custom hook appears in the logs after the next scheduled tick.
Symptoms and Fast Triage Commands
Before changing anything, confirm the failure. Typical signs are scheduled posts stuck in “Missed schedule”, WooCommerce emails not sending, backup plugins skipping runs, or actions in wp_options under cron never advancing.
SSH into the server and run these with WP-CLI commands worth automating in 2026 installed:
wp cron event list
wp cron test
wp option get cron
wp eval 'var_dump(wp_next_scheduled("my_hook"));'
If wp cron event list shows a next run time in the past and the due now column is yes, events are stuck. If the list is empty but you registered hooks, your plugin or theme never scheduled them.
Why It Depends on Traffic
WP-Cron is not a real daemon. WordPress implements it in wp-includes/cron.php with the function spawn_cron(), which is hooked to init. Every front-end or admin request loads WordPress, reaches init, and checks whether any scheduled timestamp has passed. If yes, it fires an HTTP request to wp-cron.php in the background using wp_remote_post() with a 0.01-second timeout and the blocking=false argument.
No request means no check. A brochure site with ten visits a day may not load WordPress for hours, so a task due at 03:00 might not execute until someone visits at 09:00. Caching makes it worse: if a page is served from Nginx FastCGI cache, Varnish, or a CDN edge, PHP never runs and init never fires. In 2026, with full-page caching standard on hosts like Kinsta and WP Engine, this is the number one cause of WP-Cron not working.
Another consequence is execution delay. Even on busy sites, the background request can be dropped if the server is under load, because the non-blocking request is best-effort. The event stays in the queue and retries on the next visit, but there is no guarantee of minute-level accuracy.
To see the dependency yourself, stop all traffic for an hour and watch wp cron event list; nothing advances. Then run wp cron event run --due-now manually and the queue drains instantly. That contrast proves the trigger is request-based, not time-based.
Step-by-Step Process to Replace WP-Cron with System Cron
- Confirm WP-CLI works and you can edit
wp-config.phpover SSH or SFTP. - Add the disable constant above the “stop editing” line in
wp-config.php. - Create a system crontab entry pointing PHP CLI at
wp-cron.phpin the site root. - Verify the system user matches the PHP-FPM owner to avoid permission errors.
- Test by running the same command manually and then checking the event list.
- Roll back by removing the constant if the site breaks, then re-enable and debug.
DISABLE_WP_CRON with System Cron
The constant DISABLE_WP_CRON stops the per-request spawn_cron() call. It does not delete events and does not stop the cron array; it only prevents WordPress from trying to run them on page load. In wp-config.php, place it before the comment block that says to stop editing:
define('DISABLE_WP_CRON', true);
Now add a real cron job. Use crontab -e as the user that owns the WordPress files, usually www-data on Debian or nginx on RHEL-based stacks. For PHP 8.3 in 2026 the command is:
*/5 * * * * /usr/bin/php8.3-cli -q /var/www/example.com/public/wp-cron.php > /dev/null 2>&1 Get a Quote
Email + requirement + budget range — we reply with a quote within 24 hours.
Replace the path with your actual document root. If you run multiple sites, use a lock to prevent overlap:
*/5 * * * * flock -n /tmp/wp-cron-example.lock /usr/bin/php8.3-cli -q /var/www/example.com/public/wp-cron.php > /dev/null 2>&1
Common pitfalls: using the wrong PHP binary (the CGI one instead of CLI) causes 502 or silent exit; wrong user leaves wp-content unwritable so the job dies; and forgetting > /dev/null floods your mailbox with cron output. To roll back, delete the crontab line and remove the constant, then the old behavior returns immediately.
If you manage many installs, our team handles this as part of custom WordPress development for clients who need reliable scheduling without touching the server themselves.
Verifying Execution
After switching, verify rather than assume. First, check the system cron ran:
grep CRON /var/log/syslog
journalctl -u cron | tail -20
Then confirm WordPress side effects. Run wp cron event list again; due-now should be empty and next-run times in the future. Schedule a test hook to prove the pipeline:
wp eval 'wp_schedule_single_event(time()+60, "my_test_hook");'
wp cron event run my_test_hook
Add a handler in a must-use plugin to log it:
add_action('my_test_hook', function () {
error_log('my_test_hook fired at ' . date('c'));
});
Tail the debug log at wp-content/debug.log (enable with WP_DEBUG and WP_DEBUG_LOG). If the entry appears within one minute of the scheduled time, the system cron is driving execution correctly. Also use wp cron test which runs the due hooks and prints results, useful for a quick smoke test.
For production monitoring, write the last run time to a transient and expose it in a health endpoint, or simply check that the recurring wp_version_check event advances daily. If it does not, the chain is broken somewhere between crontab and wp-cron.php.
Stuck Events and How to Clear Them
A stuck event is one whose timestamp has passed but whose hook either fails silently or never completes, so it stays in the cron option array. Causes include a fatal error inside the callback, a timeout because the task is too heavy for the 0.01-second non-blocking request, or an infinite loop in a plugin.
List them with due-now filter:
wp cron event list --fields=hook,next_run,recurrence --format=table
To clear a specific one:
wp cron event delete my_stuck_hook
To reschedule everything cleanly, first back up the option, then unschedule and let plugins re-add on next load:
wp option update cron_backup "$(wp option get cron --format=json)"
wp cron event delete --all
Be careful: --all removes events added by WooCommerce, WordPress core (wp_version_check, wp_update_plugins), and your own code. Core and well-written plugins re-schedule on the next request or admin visit, but some custom code does not. Always keep the JSON backup above so you can restore with wp option update cron '...'.
Another approach for a single hook is to unschedule then schedule again:
wp eval 'wp_unschedule_hook("my_hook"); wp_schedule_event(time(), "hourly", "my_hook");'
If events keep sticking, increase PHP max_execution_time for CLI, or split the work into smaller batches using Action Scheduler (bundled with WooCommerce since 3.5) which stores jobs in a custom table and handles retries far better than WP-Cron.
Prevention and Ongoing Maintenance
Once system cron is in place, prevent regression. Keep DISABLE_WP_CRON in version control with the rest of your config template so new environments inherit it. Use configuration management (Ansible, Puppet, or a simple deploy script) to write the crontab line, because manual edits are lost on server rebuilds.
Monitor with a synthetic check: a small script that queries wp cron event list every ten minutes and alerts if any due-now entry persists beyond fifteen minutes. On hosting without shell access, use the provider’s cron panel and point it at https://example.com/wp-cron.php?doing_wp_cron — but note that still relies on the web server being up, so it is second best.
Avoid heavy work inside cron callbacks. Offload to a queue. And when you plan a migration, review how to choose WordPress migration services in 2026 so cron jobs move with the site; forgetting to recreate crontab entries after moving servers is a classic repeat of the same failure.
Action Checklist

- Run
wp cron event listto confirm which hooks are overdue. - Add
define('DISABLE_WP_CRON', true);to wp-config.php. - Install a system crontab entry every 5 minutes using PHP CLI.
- Verify with
wp cron testand a logged test hook. - Back up the cron option before deleting stuck events.
Get Expert Help
Need reliable scheduling on a high-traffic site? Our engineers set up system cron, clear stuck queues and monitor execution. See our WordPress development services for a fixed-scope audit.
Frequently asked questions
Why does WP-Cron not work on my cached site?
Full-page caching serves HTML without loading PHP, so the init hook that triggers spawn_cron() never runs. Disable it and use system cron.
Can I just ping wp-cron.php from an external monitor?
Yes, as a fallback, but it is less reliable than localhost cron because it depends on the network and web server. Use it only when you lack shell access.
How do I know the system cron actually executed my hook?
Add an error_log() line in the hook callback, enable WP_DEBUG_LOG, then check wp-content/debug.log for the timestamp after the cron tick.
