WP-Cron is misnamed. It is not Linux cron - it is a WordPress mechanism that checks on every HTTP request whether any scheduled task is overdue and runs it inside the same PHP process. Translation: if no one visits the site for an hour, no scheduled job runs for an hour. That breaks low-traffic sites, production sites with critical jobs (backups, report emails, subscription renewals), and any site behind a page cache that bypasses PHP for most requests.
Why this matters
Things that depend on WP-Cron include scheduled posts (a 9:00 article that publishes at 9:47 because no one visited), WooCommerce transactional emails (order confirmations arriving an hour late), UpdraftPlus daily backups that simply skip days, transient cleanup that lets the database bloat, and plugin update checks that fall behind.
There is also a performance penalty. Whichever visitor "loses the lottery" pays the cost of running every overdue job in their own request - they get a 2-3 second TTFB instead of 200ms. A WooCommerce store with thousands of accumulated Action Scheduler tasks dishes out random slowness to innocent shoppers. Moving to system cron offloads that work to a separate process that never touches a visitor.
How to detect
Install Query Monitor or WP Crontrol. In WP Crontrol's Tools > Cron Events page, any job whose Next Run is in the past has missed its window. You can also read the last cron run timestamp:
echo human_time_diff(get_option('cron_last_run', 0));Another tell: WordPress's Site Health screen reporting "A scheduled event has failed" - that is almost always WP-Cron drift.
How to fix
Step one: disable the built-in WP-Cron. In wp-config.php, before the /* That's all, stop editing! */ line, add:
define('DISABLE_WP_CRON', true);Step two: configure a real system cron. cPanel exposes "Cron Jobs"; Plesk has "Scheduled Tasks"; Hostinger and SiteGround ship dedicated panels. Schedule the job every five minutes:
*/5 * * * * wget -q -O - https://example.com/wp-cron.php?doing_wp_cron > /dev/null 2>&1Or, if wget is unavailable:
*/5 * * * * curl -s https://example.com/wp-cron.php?doing_wp_cron > /dev/null 2>&1With SSH access and WP-CLI installed, the cleanest variant is:
*/5 * * * * cd /var/www/html && /usr/local/bin/wp cron event run --due-now > /dev/null 2>&1For WooCommerce stores with heavy Action Scheduler usage, drop the interval to one minute.
Common mistakes
Mistake one: setting DISABLE_WP_CRON but never configuring system cron, leaving the site with no scheduler at all. Always verify both halves. Mistake two: hitting wp-cron.php without the ?doing_wp_cron parameter - some security plugins block bare requests to that file precisely to prevent abuse. Mistake three: scheduling at one-minute intervals on a site whose cron run takes longer than a minute. Two cron processes overlap, race on the database, and either double-fire jobs or deadlock each other. Five minutes is a safe default.
Verifying the fix
Wait ten minutes after the change. In WP Crontrol, confirm Last Ran timestamps are updating. Schedule a test post for two minutes from now and verify it publishes on time. cPanel and Plesk both expose a Last Run timestamp on the cron job itself - confirm it actually fires. If it does not, double-check the URL (correct domain, valid HTTPS, no basic-auth shield, no Cloudflare WAF rule blocking the request).
wp cron event run --due-now over SSH via WP-CLI. It is faster than wget (no front-end bootstrap), unaffected by page caches, and never makes an outbound network call.