WordPress's scheduling system — used for publishing scheduled posts, checking for updates, sending scheduled emails, cleaning up transients, and any plugin-defined scheduled task — is not a real system-level cron job by default, despite the name "WP-Cron" suggesting otherwise. Understanding the actual mechanism explains most of the confusing symptoms it causes.
How WP-Cron actually works
By default, WordPress checks whether any scheduled task is due on every page load — a visitor requesting any page triggers a check of wp_options for due cron events, and runs them if any are found (via an async request WordPress fires to wp-cron.php). There's no persistent background process; scheduling is entirely piggybacked on regular site traffic.
Why this causes real problems
- Low-traffic sites delay scheduled tasks. If nobody visits the site, nothing triggers the check — a scheduled post meant to publish at 9am might not actually publish until the next visitor arrives, however much later that is.
- High-traffic sites can over-trigger it. Every page load technically attempts to check for due tasks (WordPress does have some built-in throttling against firing the check on truly every request), which adds unnecessary overhead on busy sites.
- Full-page caching makes it worse. If a page is served from cache, WordPress's PHP never actually executes for that request — so the WP-Cron check embedded in the request cycle never fires either. Heavily cached sites (which is most well-optimized sites) can end up with WP-Cron rarely triggering in practice.
- It's not exactly on schedule anyway. Even under ideal conditions, this design means "due" tasks run on the next qualifying request, not at the precise scheduled time — for most use cases that's fine, but it's worth knowing it was never a precision guarantee.
The fix: disable WP-Cron's traffic-triggered behavior and use real system cron
Add to wp-config.php:
define('DISABLE_WP_CRON', true);
Then set up an actual server-level cron job (via your host's cron scheduling, or a real crontab entry) to trigger wp-cron.php on a fixed interval:
*/5 * * * * curl -s https://example.com/wp-cron.php > /dev/null 2>&1
This decouples scheduled task execution from visitor traffic entirely — tasks run reliably on the interval you set, regardless of whether the site had any visitors, and regardless of full-page caching serving most requests from cache.
Choosing the right interval
Every 5–15 minutes is a reasonable default for most sites — frequent enough that scheduled posts and time-sensitive tasks aren't meaningfully delayed, infrequent enough not to add unnecessary load. Sites with more time-sensitive scheduling needs (frequent scheduled publishing, time-critical plugin tasks) can go tighter; low-activity sites can go looser.
Managed hosting often already handles this
Many managed WordPress hosts disable traffic-triggered WP-Cron and run it via real server cron automatically as part of their platform — worth checking your specific host's documentation or dashboard before manually configuring DISABLE_WP_CRON, since setting it up twice (both host-managed and self-configured) can create duplicate execution.
Debugging "my scheduled task didn't run"
- Check whether
DISABLE_WP_CRONis set and whether a real cron job is actually configured to hitwp-cron.php— a common failure is disabling traffic-triggered cron without setting up the replacement, which stops scheduling entirely. - Verify the scheduled event is actually registered as expected (a WP-CLI cron-listing command, or a debugging plugin, can show pending scheduled events).
- Confirm the site isn't blocking its own
wp-cron.phprequests via a firewall rule or security plugin that (incorrectly) treats the cron endpoint as suspicious traffic.
The takeaway
WP-Cron's traffic-dependent design is a reasonable default for a system with no hosting requirements, but it's the wrong choice once a site has real caching or scheduling precision needs — which is most production sites. Disabling the traffic-triggered version and running it from real system cron on a fixed interval is a small, permanent fix for an entire category of "why didn't this run on time" bugs.
