When you encounter WordPress WP-Cron not running, scheduled posts stay stuck in "Missed Schedule" status, WooCommerce subscriptions fail to renew, and scheduled database maintenance halts entirely. Default WordPress cron does not run as a true system daemon; it triggers via an asynchronous HTTP request when a visitor loads a page. This guide walks through diagnosing the root cause and setting up a reliable system-level crontab to ensure scheduled tasks fire on time.
Quick answer
- Check your cron queue immediately using WP-CLI (
wp cron event list) to identify stuck hooks or missed run times. - Check whether
DISABLE_WP_CRONis set totrueinsidewp-config.phpwithout an accompanying server cron. - Test loopback requests via the WordPress Site Health tool (
Tools > Site Health) to ensure the server can reach its own domain over HTTP/HTTPS. - Replace pseudo-cron entirely by defining
DISABLE_WP_CRONastrueand adding a 5-minute system crontab usingwp cron event run --due-now.
Step 1: Diagnose your cron queue using WP-CLI
Before changing configurations, verify whether the events exist in the database and whether they have passed their execution timestamps.
Log in via SSH and run this command from your WordPress root directory:
wp cron event list
This outputs a table showing the hook name, next execution time, relative schedule, and recurrence. If the next_run_gmt column shows dates in the past, WordPress knows tasks are due, but the internal runner is failing to execute them.
To test if the hooks can execute without errors, manually run the pending tasks right now:
wp cron event run --due-now
If the command runs successfully and clears the backlog, your hooks and callbacks are functional. The issue lies with how WordPress triggers wp-cron.php. If the command throws a fatal PHP error or hangs indefinitely, a hooked function has an infinite loop or memory exhaustion bug that you must resolve in your theme or plugin code.
Step 2: 5 causes for WordPress WP-Cron not running
1. Inadequate traffic or static full-page caching
WordPress checks for scheduled tasks during standard page requests by spawning a non-blocking POST request to /wp-cron.php. Low-traffic sites lack the visits needed to fire tasks reliably.
Aggressive caching architectures (Varnish, Cloudflare Cache Reserve, or Fastly) make this worse. If static HTML is served directly from edge caches to unauthenticated users, WordPress core never executes, meaning wp-cron.php is never triggered.
2. Failed HTTP loopback requests
To execute tasks, your server sends an HTTP request back to itself (https://yourdomain.com/wp-cron.php). If your server cannot resolve its own domain or blocks outbound requests, the execution fails silently.
Common causes of loopback failure include: - Misconfigured /etc/hosts files on the server. - Staging environments protected by HTTP Basic Authentication (.htpasswd). - Invalid or self-signed SSL certificates in local or staging environments. - ModSecurity or Web Application Firewalls (WAF) blocking requests originating from the server's own IP address.
Test loopback connectivity directly from the server terminal:
curl -I -X POST https://yourdomain.com/wp-cron.php?doing_wp_cron
A healthy response returns an HTTP 200 status. If it returns 401 Unauthorized, 403 Forbidden, or hangs on connection timeout, your loopback path is broken.
3. The DISABLE_WP_CRON constant is active
Inspect your wp-config.php file for this declaration:
define( 'DISABLE_WP_CRON', true );
Developers or hosting providers often add this constant to disable default visitor-driven execution. However, if no system cron job was scheduled at the server level to replace it, WordPress will never run any background tasks.
4. Cron lock timeouts and hung workers
To prevent race conditions where multiple requests trigger the same task simultaneously, WordPress sets a temporary transient lock named doing_cron.
If a long-running process exceeds execution limits or crashes mid-run, this lock can persist. While WordPress releases locks automatically after 10 minutes (600 seconds) by default, severe PHP fatal crashes can leave stuck records in the wp_options table.
Inspect the lock status via WP-CLI:
wp option get doing_cron
If it returns a UNIX timestamp within the last 10 minutes and no cron jobs are actually running, an event is hanging.
5. Custom hook argument mismatches
If custom scheduled events are missing, verify your hook registration. A common bug occurs when an event is registered with arguments using wp_schedule_event(), but the action hook listener expects a different signature or fails to pass arguments during unregistration, creating phantom duplicate rows.
Here is how to register a recurring custom cron event properly in WordPress:
add_action( 'init', 'prefix_register_daily_sync' );
function prefix_register_daily_sync(): void {
if ( ! wp_next_scheduled( 'prefix_execute_nightly_catalog_sync' ) ) {
wp_schedule_event( time(), 'daily', 'prefix_execute_nightly_catalog_sync' );
}
}
add_action( 'prefix_execute_nightly_catalog_sync', 'prefix_run_catalog_sync' );
function prefix_run_catalog_sync(): void {
// Execute sync logic safely
set_time_limit( 300 );
// Example: update an option to record the last successful execution
update_option( 'prefix_last_sync_timestamp', time(), false );
}
Never callwp_schedule_event()without wrapping it in awp_next_scheduled()conditional check. Calling it unconditionally oninitinjects a new row into thecronarray on every single request, bloating thewp_optionstable and degrading performance.
Step 3: Disable visitor-driven execution in wp-config.php
Do not rely on front-end traffic for business-critical jobs like e-commerce renewals or webhook processing. The production standard is disabling the visitor-based trigger completely.
Open wp-config.php and add this constant right before the line that reads / That's all, stop editing! Happy publishing. /:
/** Disable the default visitor-driven pseudo cron runner */
define( 'DISABLE_WP_CRON', true );
Once added, WordPress will no longer initiate internal HTTP loopback requests on user visits. Now, configure a real server cron.
Step 4: Configure a system crontab on your server
You have two primary methods to run scheduled tasks via the operating system: using WP-CLI (recommended) or using curl.
Access your server via SSH as the user that owns the web files (e.g., www-data, nginx, or your cPanel username). Open the crontab editor:
crontab -e
Option A: WP-CLI (Recommended) Running cron via WP-CLI bypasses web server timeouts, runs directly in PHP CLI, and consumes significantly less memory because it does not initialise web server middleware or send network packets.
Add this line to run due events every 5 minutes:
*/5 * * * * /usr/local/bin/wp cron event run --due-now --path=/var/www/html >/dev/null 2>&1
Ensure you replace /usr/local/bin/wp with the absolute path to your WP-CLI binary (found via which wp) and /var/www/html with the path to your WordPress root.
Option B: System cURL (Fallback)
If you are on shared hosting where WP-CLI is unavailable or restricted, invoke wp-cron.php using curl:
*/5 * * * * curl -s -L https://yourdomain.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1
The ?doing_wp_cron parameter is required. It tells WordPress to bypass the DISABLE_WP_CRON constant check inside wp-cron.php and execute the pending queue.
Step 5: Handle staging sites and basic authentication
Staging sites protected by HTTP Basic Authentication frequently break wp-cron.php if using the HTTP/cURL approach because the external request gets a 401 Unauthorized header.
If you must use curl on a password-protected environment, pass the credentials within the cron command:
*/10 * * * * curl -s -u username:password https://staging.yourdomain.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1
Alternatively, avoid the network stack completely by using the WP-CLI method shown in Option A, which operates at the filesystem and database layer and remains unaffected by HTTP authentication.
Step 6: Monitor stuck cron tasks programmatically
On mission-critical platforms, silent cron failures lead to lost revenue. You can monitor overdue jobs and log errors if any event becomes stuck for more than two hours.
Place this snippet inside a custom site plugin or mu-plugins file:
<?php
/**
* Plugin Name: Cron Health Sentinel
* Description: Monitors overdue cron events and logs warnings.
*/
declare(strict_types=1);
add_action( 'prefix_monitor_cron_health', 'prefix_check_overdue_crons' );
function prefix_check_overdue_crons(): void {
$crons = _get_cron_array();
if ( empty( $crons ) ) {
return;
}
$now = time();
$threshold = 2 * HOUR_IN_SECONDS;
foreach ( $crons as $timestamp => $hooks ) {
if ( ( $now - $timestamp ) > $threshold ) {
foreach ( $hooks as $hook => $events ) {
error_log(
sprintf(
'[CRON WARNING] Event "%s" is overdue by %d seconds.',
sanitize_text_field( $hook ),
$now - $timestamp
)
);
}
}
}
}
This provides an early warning in your server's debug log whenever a background process fails to clear from the queue.
Checklist
- Run
wp cron event listto confirm whether tasks are stuck or missed. - Manually trigger
wp cron event run --due-nowto rule out fatal PHP execution errors. - Check
wp-config.phpforDISABLE_WP_CRON. - Run a manual loopback test with
curl -I -X POSTagainstwp-cron.php. - Ensure staging environments with Basic Auth supply credentials or use WP-CLI.
- Verify
wp_schedule_event()calls are always wrapped insidewp_next_scheduled()checks. - Add
define( 'DISABLE_WP_CRON', true );towp-config.php. - Add a 5-minute system crontab entry using WP-CLI or curl.
Frequently asked questions
Why are my WordPress scheduled posts missing their schedule?
WordPress misses scheduled posts when no visitors load the website around the planned publishing time, or when page caching plugins prevent the application from loading. Because default WP-Cron requires visitor traffic to initiate tasks, low-traffic or cached sites fail to fire the internal publishing hook.
How often should a server cron run for WordPress?
A 5-minute interval (`*/5 * * * *`) is the standard recommendation for most production sites. For large WooCommerce stores or high-volume publishing sites handling time-sensitive webhooks and transactional emails, run the cron job every 1 to 2 minutes to prevent queue backlogs.
Is WP-CLI better than cURL for system cron?
Yes. WP-CLI executes PHP code directly via the command line interface without sending an external HTTP network request. This bypasses web server timeouts, ignores HTTP basic authentication, avoids SSL handshake issues, and avoids consuming web server connection pools.
Does DISABLE_WP_CRON turn off scheduled events completely?
No, setting `DISABLE_WP_CRON` to `true` only stops WordPress from running background checks during public page visits. The underlying cron queue remains in the database and executes whenever `wp-cron.php` is pinged directly or when tasks are triggered through WP-CLI.
- Advanced Administration HandbookAdvanced Administration Handbook
- WP Engine supportWP Engine support
Drafted with AI assistance. Code targets current WordPress, WooCommerce and Next.js APIs; test changes on a staging site before production.