Sheraz AhmadSenior Web Developer
Let's talk

WordPress WP-Cron Not Running: 5 Causes and How to Fix It

Diagnose why WordPress scheduled tasks fail, fix loopback or caching blocks, and migrate safely to a real server cron job.

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_CRON is set to true inside wp-config.php without 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_CRON as true and adding a 5-minute system crontab using wp 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 call wp_schedule_event() without wrapping it in a wp_next_scheduled() conditional check. Calling it unconditionally on init injects a new row into the cron array on every single request, bloating the wp_options table 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 list to confirm whether tasks are stuck or missed.
  • Manually trigger wp cron event run --due-now to rule out fatal PHP execution errors.
  • Check wp-config.php for DISABLE_WP_CRON.
  • Run a manual loopback test with curl -I -X POST against wp-cron.php.
  • Ensure staging environments with Basic Auth supply credentials or use WP-CLI.
  • Verify wp_schedule_event() calls are always wrapped inside wp_next_scheduled() checks.
  • Add define( 'DISABLE_WP_CRON', true ); to wp-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.

Reference documentation

Drafted with AI assistance. Code targets current WordPress, WooCommerce and Next.js APIs; test changes on a staging site before production.

Need a fast, modern website?

I'm Sheraz Ahmad, a senior WordPress and headless Next.js developer with 7+ years of shipping high-performance sites. Open to permanent roles, contracts and freelance projects.

Let's talk