How to Fix WordPress Missed Schedules and WP-Cron Problems
A practical guide to diagnosing missed scheduled posts and other WP-Cron failures: how the queue works, common causes, WP-CLI checks, and switching to a real system cron.
Why “missed schedule” happens
You publish a post for 8:00 tomorrow morning, come back at 10:00, and the dashboard still says “Missed schedule.” Nothing crashed. The editor works, the site works — the post simply never went out. In most cases the culprit is not a broken plugin but the way WordPress runs time-based work in the first place.
The official Plugin Handbook is blunt about it: WP-Cron is only triggered on page load. On every request, WordPress checks the list of scheduled tasks and runs whatever is due during that same request. If nobody loads a page between 8:00 and the moment you check, the task stays in the queue. The handbook’s own example is a task scheduled for 2:00 PM that does not run until 5:00 PM because no page load happened in between.
That design is a deliberate trade-off. Shared hosting rarely gives you access to a system scheduler, and the WordPress API is simpler for plugins than shelling out to cron. The queue also guarantees that a missed task will run eventually rather than being skipped forever, which is what a system cron would do. But the flip side is that timing is never guaranteed, and on a low-traffic site — a site whose only visitor some nights is a search bot — scheduled posts, expired transients, update checks and plugin maintenance jobs can all drift by hours.
Before you can fix this, you need a clear picture of what WP-Cron actually does. The handbook’s companion page on scheduling explains that WP-Cron does not use calendar times the way a system cron does. It uses intervals: an initial run time plus a repeat interval measured in seconds. WordPress ships with four built-in intervals — hourly, twicedaily, daily, and weekly — and plugins can register their own through the cron_schedules filter.
Step 1: Confirm the event actually exists
Start by listing what is in the queue. With WP-CLI, one command shows every scheduled event, its next run time and its recurrence:
wp cron event list --fields=hook,next_run_gmt,next_run_relative,recurrence
Look for two things. First, that the event you expect — publish_future_post, in the case of a scheduled post — is present at all. Second, whether next_run_relative is stuck in the past, which tells you the queue is populated but nothing is draining it.
If you maintain sites this way routinely, the same tooling fits into a scripted maintenance workflow like the one described in our guide to updating WordPress core, plugins and themes safely with WP-CLI. You can trigger all due events manually to separate “the task is broken” from “the task never gets a chance to run”:
wp cron event run --due-now
If the scheduled post publishes the moment you run that command, the callback is fine and your problem is purely the trigger. If it still fails, the problem is inside the callback or a conflict, and you should move on to error logging rather than cron configuration.
Step 2: Test whether wp-cron.php can run
WP-Cron work happens in a request to wp-cron.php, which WordPress spawns as a loopback request to your own server. This is where many “missed schedule” cases actually break. Ask the site for the file directly:
https://example.com/wp-cron.php?doing_wp_cron
A plain 200 response with an empty body is what you want. Common failure patterns instead:
- A firewall or security module blocks loopback requests. The web server connects to itself, which some host-level rules treat as suspicious traffic.
- The host cannot resolve its own domain, so the loopback request never completes.
- A caching layer serves
wp-cron.phpfrom cache without the request ever reaching PHP. DISABLE_WP_CRONis already defined inwp-config.php— usually left behind by a previous administrator or a hosting installer — which turns off the page-load trigger entirely.
Check wp-config.php for that constant before anything else. It is a legitimate setting, but only when something else calls wp-cron.php, and an orphaned define( 'DISABLE_WP_CRON', true ); with no system cron behind it will produce exactly the symptoms you are debugging.
Also enable error display for the cron context while you are testing: WP_DEBUG and WP_DEBUG_LOG in wp-config.php will write fatal errors from cron callbacks to debug.log, and a plugin whose scheduled callback throws a fatal error can block the whole queue run — a failure mode that looks like a scheduling problem but is really a code problem. Diagnosing that class of errors is the same discipline we describe in fixing WordPress 500 errors: read the log first, guess second.
Step 3: Replace page-load triggering with a real cron
Once you have confirmed the queue is intact, the fix recommended by the handbook itself is to stop relying on visitors. Have the operating system’s task scheduler request wp-cron.php at a fixed interval instead.
On Linux and macOS, open the crontab with crontab -e and add an entry every fifteen minutes:
*/15 * * * * wget --delete-after https://example.com/wp-cron.php
The --delete-after flag discards the downloaded response, since you only care about triggering the script, not saving its output. On Windows, create a Basic Task in Task Scheduler and point it at a PowerShell one-liner such as powershell "Invoke-WebRequest https://example.com/wp-cron.php". Both examples come straight from the handbook page on hooking WP-Cron into the system task scheduler.
Then — and only then — disable the page-load trigger in wp-config.php, above the “stop editing” line:
define( 'DISABLE_WP_CRON', true );
The order matters. If you set the constant first and the system entry is wrong or silently failing, every scheduled task on the site stalls indefinitely, including update checks and cleanup jobs. Set up the cron entry, verify it fires by watching wp cron event list for a few intervals, and only then disable the built-in trigger. Keep in mind that scheduled work will now run with the frequency you chose: a fifteen-minute cron means a post scheduled for 8:00 sharp can appear at up to 8:15. For most content sites that is a better failure mode than “hours later,” but if you need minute precision, schedule posts for times aligned with your interval or shorten it.
Step 4: Clean up the queue itself
With triggering fixed, look at what is in the queue. Two problems are common:
Duplicate events. Plugins that call wp_schedule_event() on every page load instead of guarding it with wp_next_scheduled() can enqueue thousands of copies of the same task. wp cron event list makes this visible instantly when one hook name appears again and again. Fix it in the responsible plugin, not by deleting events by hand, because the code will re-create them on the next load. The handbook is explicit that scheduling code should always check wp_next_scheduled() before adding an event.
Orphaned events. Poorly written plugins also fail to unschedule their tasks when deactivated, so WordPress keeps trying to run hooks whose callbacks no longer exist. These rarely cause missed schedules on their own, but they add noise and load to every queue run. If you identify the plugin, the correct remedy is the plugin’s own cleanup routine or wp_unschedule_event() / wp_clear_scheduled_hook() from a small must-use plugin — and a note to the plugin author.
When WP-Cron is the wrong tool entirely
A page-load scheduler is fine for publishing posts and refreshing transients. It is the wrong tool for anything that must happen at a known time or that processes significant data: order exports, membership renewals, newsletter batches, nightly inventory syncs. Those jobs belong to a real system scheduler or a queue service, with WordPress code invoked as a worker. Deciding where that line sits — and what it costs to operate the stack around it — is part of the platform maintenance budget we discuss in the total cost of ownership of running a business website.
If maintaining this by hand is more than you want to own, our WordPress development service covers scheduled-task architecture, loopback debugging and maintenance automation as standard work, and the WordPress platform page describes what we run on that stack in general.
A checklist you can reuse
- Run
wp cron event listand confirm the event exists and is due. - Run
wp cron event run --due-now; if the task succeeds, the trigger is the problem. - Request
wp-cron.php?doing_wp_crondirectly and check for a200. - Grep
wp-config.phpforDISABLE_WP_CRON. - Enable
WP_DEBUG_LOGand checkdebug.logfor fatal errors in callbacks. - Add a system cron or Task Scheduler entry hitting
wp-cron.phpevery 5–15 minutes. - Verify the queue drains for at least two intervals, then set
DISABLE_WP_CRON. - Re-check
wp cron event listfor duplicates and orphans once a quarter.
Most missed-schedule tickets close at step 3 or step 6: either a blocked loopback request or a site too quiet to trigger its own queue. The remaining cases are almost always a fatal error inside a callback — a code problem wearing a scheduling costume. Work the list in order and you will land in the right bucket every time.
Official sources.
Use these references to verify platform-specific behavior and version details.
Continue with the surrounding work.
How to Troubleshoot a WordPress 500 Internal Server Error
How to Fix the WordPress Media Upload HTTP Error for Good
Broader context from the YCTheme blog.
Total Cost of Ownership: What a Business Website Actually Costs to Run
A three-year cost framework for WordPress, Joomla, TYPO3 and Shopify websites, covering hosting, upgrades, security, extensions and the labor nobody budgets for.
Staging Environments: How Professional Teams Ship CMS Changes Without Breaking Production
Staging is where risky changes prove themselves before customers see them. A practical look at how WordPress, Joomla, TYPO3 and Shopify teams stage, test and promote changes.