WP-Cron isn’t cron
The single most misunderstood piece of WordPress. WP-Cron is a job queue that fires on page load. No visitor, no cron. On a busy site it “just works.” On a staging environment or a 200-visits-a-day blog, scheduled posts miss their publish time, transient garbage collection stops, and plugin cleanup jobs pile up until the options table bloats.
How it actually works
On every request, WordPress checks wp_options for the cron option, compares timestamps, and if anything is due, spawns a non-blocking loopback HTTP request to /wp-cron.php. The current request finishes; the loopback processes the queue. Silent failure modes: firewall blocks the loopback, DNS resolution loops on the loopback hostname, or PHP FPM has no free workers so the loopback times out. All three fail without a single log line in debug.log by default.
When to leave WP-Cron alone
- Site gets steady traffic (roughly >1 request per minute during business hours)
- No scheduled posts with tight time windows
- No time-sensitive third-party syncs (payments, CRM webhooks)
- You’re on shared hosting with no shell access
When to move to system cron
- Low-traffic or staging site
- You’ve seen a scheduled post publish late
- WooCommerce with subscription renewals
- Any plugin that queues background jobs (Action Scheduler, WP Background Processing)
- You’re on a VPS or managed host with cron access (Kinsta, WP Engine, Rocket.net all support this)
The switch, step by step
1. Disable WP-Cron in wp-config.php:
define('DISABLE_WP_CRON', true);
2. Add a system cron entry. Run every 5 minutes for most sites; every 1 minute if you have subscription renewals or time-sensitive integrations:
*/5 * * * * cd /var/www/html && /usr/bin/wp cron event run --due-now > /dev/null 2>&1
Use WP-CLI, not a curl to wp-cron.php. CLI runs in a proper PHP CLI process with its own memory limit and time limit — no worker starvation, no loopback firewall issues, and errors go to stderr so your log actually catches them.
3. Verify. Run wp cron event list and confirm the next-run timestamps advance after each cron tick. Watch for 24 hours before assuming success.
Managed host specifics (2026)
- Kinsta: Auto-disables WP-Cron on all plans and runs a system cron every 15 minutes. Request a shorter interval in support if you need it.
- WP Engine: Alternative Cron enabled by default. For 1-minute precision, use the User Portal > Utilities > Alternative Cron settings.
- Cloudways: Add the cron entry via the “Application Settings > Cron Job Management” panel. WP-CLI path is
/usr/bin/php /home/master/applications/<app>/public_html/wp-cli.phar. - SiteGround: Shared plans don’t allow disabling WP-Cron reliably. Upgrade to Cloud or move.
Debugging a job that never runs
wp cron event list— is it even scheduled?wp cron event run <hook>— does it succeed when forced?- Check
WP_DEBUG_LOGfor PHP errors thrown by the callback - Confirm the hook is registered on every request, not conditionally inside
is_admin()— one of the most common bugs I see - Look for double-registration: two plugins scheduling the same hook name at different intervals
Action Scheduler note
WooCommerce and many modern plugins now use Action Scheduler instead of raw WP-Cron. It stores jobs in its own tables and still relies on WP-Cron to trigger the runner. Moving to system cron helps Action Scheduler just as much as it helps native jobs — sometimes more, because Action Scheduler queues get long and a stalled runner is invisible in the WP admin.
Bottom line
If your site has real traffic, ignore this post and enjoy your day. Otherwise, disable WP-Cron, add a five-minute system cron running WP-CLI, and watch the entire class of “why didn’t my post publish” tickets disappear.





