Written by 10:36 am Debugging & Profiling, WordPress Views: 0

WordPress WP-Cron Not Running? How to Debug Scheduled Tasks

WP-Cron stops running silently and the cause is almost always a stuck lock, a blocked loopback request, or DISABLE_WP_CRON set without a server cron replacement. This guide covers the full diagnostic flow with WP-CLI, _get_cron_array(), WP_CRON_LOCK_TIMEOUT, object cache conflicts, and real cron setup.

WordPress WP-Cron debugging guide - diagnostic flow, WP_CRON_LOCK_TIMEOUT, WP-CLI commands, and cron failure causes

WordPress relies on WP-Cron to run scheduled tasks – sending emails, publishing scheduled posts, running plugin maintenance jobs, and dozens of other background operations. When WP-Cron stops working, these tasks silently pile up. Posts miss their publish time. Backup plugins skip runs. Membership renewals never fire. The site keeps loading fine, so you have no idea anything is broken until you check and find dozens of missed events sitting in the queue.

This guide covers the full diagnostic flow: how WP-Cron actually works under the hood, why it fails, how to inspect the cron queue directly with _get_cron_array() and WP-CLI, how to fix the most common causes including WP_CRON_LOCK_TIMEOUT, object cache conflicts, and hosting restrictions, and how to replace WP-Cron entirely with a real server cron job.


How WP-Cron Actually Works

WP-Cron is not a real operating system cron. There is no daemon running in the background on a timer. Instead, WordPress piggybacks on page loads. Every time someone visits your site, WordPress checks whether any scheduled tasks are due. If they are, it spawns a non-blocking HTTP request to wp-cron.php and runs those tasks in the background.

This design has a fundamental weakness: if no one visits your site, no cron runs. On low-traffic sites, this means scheduled tasks can be hours late. On very low-traffic sites (staging environments, internal tools), tasks may never run at all.

The flow looks like this:

  1. A visitor loads a page on your site.
  2. WordPress calls spawn_cron() inside wp-includes/cron.php.
  3. spawn_cron() checks whether the current time is past any scheduled event timestamps stored in the cron option (retrieved via _get_cron_array()).
  4. If tasks are due, WordPress checks for a lock transient called doing_cron. If no lock exists, it sets one and fires a non-blocking HTTP request to wp-cron.php.
  5. wp-cron.php runs the due hooks, then clears the lock.
  6. WordPress reschedules recurring events with their next run time.

The entire system depends on three things working correctly: HTTP requests to wp-cron.php being reachable, the doing_cron transient not getting stuck, and the cron option in the database being writable. Any one of these failing silently breaks all scheduled tasks.


Step 1: Confirm WP-Cron Is Actually Broken

Before diving into fixes, confirm the problem is real. Sometimes developers assume cron is broken when a plugin just has a long interval between runs.

Check the cron queue with WP-CLI

WP-CLI gives you the most direct view of what is queued and what is overdue:

From the command line, these WP-CLI commands give the same information without writing any PHP:

wp cron event list – lists all scheduled hooks, their schedule name, next run time, and whether they are overdue.
wp cron event run –due-now – manually fires all events that are currently due.
wp cron test – tests whether the cron system can actually spawn HTTP requests.

Run wp cron test first. If it returns an error about HTTP requests failing, you have a connectivity problem – WP-Cron cannot reach wp-cron.php via HTTP. This is common on servers with loopback restrictions or on sites behind certain firewalls.

Inspect _get_cron_array() directly

You can also dump the cron array from WP-CLI’s eval mode:

wp eval 'print_r(_get_cron_array());'

This prints the raw contents of the cron option: a nested array of Unix timestamps pointing to hook names and their arguments. Any event with a timestamp older than the current time is overdue and should have already run.


Common Cause 1: WP_CRON_LOCK_TIMEOUT

WP_CRON_LOCK_TIMEOUT is a constant that controls how long WordPress considers a cron lock valid. The default is 60 seconds.

Here is what happens: when cron runs, it sets the doing_cron transient with the current Unix timestamp as its value. Before spawning a new cron run, WordPress checks this transient. If the stored timestamp is less than WP_CRON_LOCK_TIMEOUT seconds ago, WordPress assumes cron is already running and skips the spawn.

The problem happens when a cron run takes longer than 60 seconds – or when the transient gets stuck due to a PHP fatal error or timeout mid-run. The lock never clears. Every subsequent page load sees the lock and skips cron. Eventually, all scheduled tasks pile up with no way to run until the transient expires or is deleted manually.

Fix 1: Increase the timeout if your cron tasks legitimately take more than 60 seconds:

Fix 2: Clear the stuck lock manually:

wp transient delete doing_cron

After deleting the transient, the next page load will spawn a fresh cron run. Monitor debug.log to confirm it fires.


Common Cause 2: DISABLE_WP_CRON Without a Real Cron Job

Many performance guides recommend setting define( 'DISABLE_WP_CRON', true ); in wp-config.php to stop WordPress from spawning cron on every page load. This is good advice for high-traffic sites. But it only makes sense if you set up a real server cron job as a replacement. The mistakes developers make with WordPress cron jobs covers exactly this pattern – disabling WP-Cron without the server-side follow-through is one of the most common cron pitfalls.

The mistake: someone adds DISABLE_WP_CRON based on a tutorial, forgets to add the server cron, and all scheduled tasks stop running silently. The site’s debug log stays empty because WP-Cron is simply turned off – no errors, just silence.

Check whether this constant is set:

wp eval 'echo defined("DISABLE_WP_CRON") && DISABLE_WP_CRON ? "DISABLED" : "ENABLED";'

If the output is DISABLED and you have no server cron running, that is your problem. The correct setup:

The WP-CLI version of the cron command (wp cron event run --due-now) is preferable to calling wp-cron.php directly via PHP. It runs within the WordPress context without needing an HTTP request, works on servers that block loopback connections, and logs output cleanly.


Common Cause 3: Object Cache Holding the Cron Lock

Redis, Memcached, and similar persistent object caches change how WordPress transients work. By default, transients are stored in the database with a timeout. With a persistent object cache active, they are stored in memory and the TTL is managed by the cache backend.

This causes a specific cron failure pattern: the doing_cron transient gets cached in Redis with the wrong TTL, or it gets cached without a TTL at all and never expires. WordPress sees the lock on every page load and never spawns a new cron run.

Diagnosing this: check how long the lock has been set by comparing its stored timestamp to the current time. If it is older than a few minutes, it is stuck:

You can also check directly in Redis if you have access to the Redis CLI:

redis-cli keys "*doing_cron*"
redis-cli ttl "your_prefix:transient_doing_cron"

A TTL of -1 means the key has no expiry – it will never clear on its own. Delete it and fix your object cache configuration to ensure transients are stored with their correct TTL.


Common Cause 4: HTTP Loopback Blocked by the Server

WP-Cron works by making an HTTP request from the server to itself – specifically to yourdomain.com/wp-cron.php. This is called a loopback request. Many managed hosting environments, cPanel-based hosts, and servers behind certain firewalls block loopback connections for security or resource reasons.

When loopback is blocked, wp cron test will return something like:

Error: There was a loopback request problem. The loopback request returned a non-200 status code.

WordPress also has a built-in REST API health check that tests loopback requests. Go to Tools > Site Health > Status and look for any warnings about background updates or scheduled events. The “Site Health” panel uses WP_HTTP::request() to test this and will flag loopback failures.

Fixes for loopback issues:

  • Whitelist the server’s own IP in the firewall – allow the server to make HTTP requests back to itself on port 80 and 443.
  • Switch to server cron – if loopback is permanently blocked, disable WP-Cron and use a real cron job with WP-CLI instead (as covered in the section above). WP-CLI does not need HTTP – it loads WordPress directly.
  • Check for BasicAuth – if your staging environment has HTTP authentication enabled, loopback requests will receive a 401 and fail. Use a filter to skip authentication for wp-cron.php requests, or switch to server cron.

Common Cause 5: Hosting Restrictions on PHP Execution Time

Shared hosting environments often enforce short PHP execution time limits (30 seconds or less) that can kill long-running cron tasks mid-execution. When this happens, the cron lock may not clear properly – the PHP process is terminated abruptly before it reaches the cleanup code.

Signs of this problem: cron appears to start (the lock is set and clears), but certain hooks never complete their work. Or the lock gets set and never clears because the PHP process died with a timeout error.

Check your PHP error log for timeout messages:

Fatal error: Maximum execution time of 30 seconds exceeded in /wp-includes/cron.php

Solutions depend on what your host allows:

  • Request a higher max_execution_time for cron-specific requests, or configure it in a .htaccess or php.ini override if your host supports it.
  • Break heavy cron tasks into smaller batches that each complete well within the time limit.
  • Move to server cron via WP-CLI, which bypasses web server PHP limits and runs under the command-line PHP binary with its own configuration.

Detecting Missed Schedules Automatically

Instead of manually checking the cron queue every time you suspect an issue, you can add a lightweight hook to detect and log missed schedules on every page load. This is useful during a debugging session – enable it, browse the site for a day, then check the log for any overdue events.

This snippet loops through every event in the cron array on each page load, compares its scheduled timestamp to the current time, and logs anything overdue to debug.log. You will see which specific hooks are being missed, how overdue they are, and how frequently.

Remove the hook once you have identified and fixed the problem – you do not want this running in production indefinitely.


Debugging with WP-CLI: The Full Workflow

WP-CLI’s cron subcommands give you full control over the cron queue without touching the database directly or writing PHP. If you are not yet familiar with reading the output from these tools, the guide on how to enable and use the WordPress debug log will help you interpret what gets written when cron runs. Here is the complete debugging workflow:

1. Test cron connectivity

wp cron test

This fires a test HTTP request to wp-cron.php and reports whether the loopback works. If this fails, fix connectivity before anything else.

2. List all scheduled events

wp cron event list

Shows all hooks in the queue with their schedule (hourly, daily, etc.), next run timestamp, and whether they are overdue. Sort by timestamp to see which events are most overdue.

3. Run all overdue events immediately

wp cron event run --due-now

Fires every hook that is currently due or overdue. Use this to manually clear a backlog after fixing the underlying problem. Add --verbose to see which hooks ran and how long each took.

4. Run a specific hook

wp cron event run wp_scheduled_delete

Run a single named hook to test whether it executes without errors. Watch for PHP warnings or fatals in the output – these are often the reason a hook fails silently during normal cron runs.

5. Delete a stuck or unwanted hook

wp cron event delete hook_name

Use this to remove orphaned hooks from deactivated plugins. Orphaned hooks sit in the queue indefinitely and create noise when debugging. If a plugin is uninstalled without properly deregistering its cron events, the hook will keep appearing as missed because there is no longer any callback registered for it.

6. List all registered schedules

wp cron schedule list

Shows all available schedule names (hourly, twicedaily, daily, weekly, plus any custom schedules added by plugins) along with their intervals in seconds. Useful for spotting custom schedules that plugins define with unreasonably short intervals.


Diagnosing Specific Plugin Cron Issues

Sometimes cron is working correctly overall but a specific plugin’s scheduled tasks are still not running. This usually means one of three things:

The plugin’s hook callback is throwing an error

If a hook’s callback function triggers a PHP warning, notice, or fatal error, the cron run may stop before rescheduling the event. The event gets removed from the queue but never re-queued, making it look like it ran but producing no results.

Enable WP_DEBUG and WP_DEBUG_LOG in wp-config.php, then manually run the specific hook via WP-CLI: wp cron event run plugin_hook_name. Watch the output for any errors. Check wp-content/debug.log for PHP notices or fatals that would not show on screen.

The plugin’s event was never registered

Plugins should register their cron events on a hook that fires when the plugin is activated (typically the activation hook or during init). If the plugin was updated and its cron registration code was refactored, the event may have been dropped from the queue without being re-added.

Check whether the hook exists: wp cron event list | grep your_hook_name. If it is not in the list, the plugin is not registering it. Deactivate and reactivate the plugin to trigger the activation hook.

The plugin’s callback is using too much memory

A cron callback that exhausts PHP memory will die without rescheduling or logging cleanly. Check the PHP error log for “Allowed memory size” fatal errors timestamped around when cron should have run. Increase WP_MEMORY_LIMIT in wp-config.php, or optimize the callback to process in smaller batches.


Quick Diagnostic Checklist

If WP-Cron is not running and you need to diagnose it systematically, go through this checklist in order:

CheckCommand / LocationWhat to Look For
Is cron disabled?wp-config.phpDISABLE_WP_CRON set to true without a server cron
Is the lock stuck?wp eval 'print_r(get_transient("doing_cron"));'Timestamp older than WP_CRON_LOCK_TIMEOUT
Are events overdue?wp cron event listAny events past their scheduled time
Is loopback working?wp cron testNon-200 response or connection refused
PHP errors in cron runs?wp-content/debug.logFatals, memory errors, timeouts
Object cache locking?Redis CLI or WP-CLI transient checkdoing_cron key with TTL of -1
PHP execution timeout?PHP error log“Maximum execution time exceeded” in cron.php

Should You Replace WP-Cron Entirely?

For most production sites with more than a few hundred daily visitors, replacing WP-Cron with a real server cron job is the right long-term move. The HTTP-spawn approach was designed for shared hosting in the early 2000s when server cron access was not common. Today, most hosting environments – from managed WordPress hosts to VPS to cloud platforms – give you cron access.

Benefits of switching to a real server cron:

  • Predictable timing – tasks run at the exact scheduled time, not “whenever a visitor shows up”.
  • No loopback dependency – WP-CLI runs in the shell, no HTTP request needed.
  • Better PHP limits – command-line PHP typically has more generous execution time and memory limits than web requests.
  • Lower request overhead – eliminates the extra HTTP request on every page load that WP-Cron normally spawns.
  • Works on low-traffic sites – staging environments, admin-only tools, and low-traffic blogs all get reliable cron regardless of visitor traffic.

The tradeoff is that you need server access. If you are on managed hosting without SSH or cron access, you are stuck with WP-Cron. In that case, the best you can do is make WP-Cron as reliable as possible by fixing any lock issues, ensuring loopback works, and keeping individual cron tasks lightweight and fast.


Monitoring Cron After Fixing It

Once you have fixed the underlying issue, add some ongoing monitoring so you catch problems early instead of discovering them after tasks have been missed for days.

WP Crontrol is a free plugin that adds a Cron Events menu to your WordPress admin, showing all scheduled events with their next run time, recurrence, and associated callback. It also lets you run, pause, and delete events from the admin interface without WP-CLI. Useful for non-technical team members who need to monitor cron without server access.

Query Monitor logs cron event execution and timing when WP-Cron runs during a page load. Combined with WP_DEBUG_LOG, this gives you a record of which hooks fired, how long they took, and whether any produced PHP errors.

Server-side cron logging: if you are using a real server cron job with WP-CLI, redirect output to a log file:

*/5 * * * * cd /var/www/html && wp cron event run --due-now >> /var/log/wp-cron.log 2>&1

Rotate this log with logrotate to keep it from growing unbounded. Review it weekly to spot any hooks that consistently fail or take longer than expected.


Summary: Why WP-Cron Stops Running

WP-Cron failures almost always come down to one of these root causes:

  1. DISABLE_WP_CRON set without a replacement – the most common cause on sites that followed an optimization guide halfway.
  2. Stuck doing_cron lock – a previous cron run crashed or timed out, leaving the lock in place. Fix: wp transient delete doing_cron.
  3. Object cache holding the lock indefinitely – Redis or Memcached caching the transient without the correct TTL. Fix: flush the specific transient from the cache backend.
  4. Loopback HTTP requests blocked – the server cannot call itself. Fix: whitelist loopback, or switch to server cron via WP-CLI.
  5. PHP execution time limit exceeded – a heavy cron task is being killed mid-run. Fix: increase the limit, batch the task, or switch to command-line PHP.
  6. Orphaned hooks from uninstalled plugins – these appear as permanently overdue events in the queue. Fix: wp cron event delete hook_name.

Most of these can be diagnosed in under five minutes with WP-CLI. Start with wp cron test, then wp cron event list, then check the doing_cron transient. Between these three checks you will find the root cause in the majority of cases.


WP-Cron failures are silent by default. Add debug logging during development and monitor the cron queue regularly on production sites – missed schedules cost you more than the few minutes it takes to set up monitoring.


Related Reading on TweaksWP

If you are working through cron issues as part of a broader WordPress performance audit, these articles cover the adjacent problems you are likely to hit:


Fix WordPress Cron Issues Before They Break Your Site

Missed schedules are one of those problems that compound quietly. A backup that did not run, a membership that did not renew, an email sequence that stalled – all from a 60-second transient that got stuck. Five minutes with WP-CLI clears most of these. The articles in the Debugging and Profiling series on TweaksWP walk through the tools and patterns that developers use to catch these issues early.

Visited 1 times, 1 visit(s) today

Last modified: April 1, 2026