Fix WP-Cron and missed schedule
You queued a post for 9:00 AM, opened your inbox to confirm it went out, and found the editor still showing the word Scheduled in calm blue. A few hours later it flips to the red label nobody wants to see: Missed schedule. Nothing published, no email reached your list, and today’s analytics report starts with a hole in it.
This has almost nothing to do with the editor, your theme, or the plugin that scheduled the post. It is wp-cron, WordPress’s built in scheduling system, and the way it works catches nearly everyone off guard the first time. I have fixed this on client sites for years, and in almost every case the owner had been blaming the wrong thing for weeks.
wp-cron is not a real scheduler. It has no timer and no background daemon. It checks for due work when someone loads a page, which means a quiet site simply does not run it. Once you understand that one sentence, the fix takes about ten minutes and you never think about it again. Here is the whole thing, including the edge cases the official docs skip.
How wp-cron Actually Works (and Why It Misses)
When WordPress saves a scheduled post, it does not set an alarm clock. It writes an entry into a serialized array stored in your database, in the wp_options table under the option name cron. That array lists every pending event and the timestamp it is due.
On every page load, WordPress compares the current time against that array. If an event is due, WordPress fires an HTTP request to wp-cron.php on your own domain. That is a “loopback” request, the site calling itself. That request runs the due events and writes the array back. That is the entire mechanism.
Three consequences fall straight out of that design:
- No visitors means no checks. A site that gets ten visits a day checks its schedule about ten times a day. A site that gets zero visits checks it zero times.
- Loopback requests have to work. If something blocks
wp-cron.php, the event never fires, and WordPress fails silently. - A run can get stuck. If a cron run is interrupted halfway, a lock called
doing_croncan stay set, and every later run bails out immediately.
That is why the “Missed schedule” label exists at all. WordPress is admitting an event was due and never ran. It is a symptom, not the cause.
Confirm It Is wp-cron (Two-Minute Diagnosis)
Before you change anything, prove the problem. Run these three checks in order and you will know exactly what you are dealing with.
1. Check Site Health
Go to Tools, Site Health, Status. Look for the scheduled events test. A late event shows up as a warning: “The scheduled event, publish_future_post, is late. Your site’s wp-cron is not working as expected.” That one line rules out the editor and points straight at cron.
2. Inspect the events with WP-CLI
Site Health tells you something is late. WP-CLI tells you which hook and when it was due. The next_run_gmt column is the tell: if it sits in the past, the event is waiting for a page load that never came.
3. Test the loopback request
Now curl the cron file from outside your server, exactly the way WordPress tries to reach it:
curl -I https://yourdomain.com/wp-cron.php
A 200 in the first line means the file is reachable. A 403, 401, or 500 means something is blocking the loopback request, and rescheduling the post will never help until you unblock it.
The Fix: Give wp-cron a Real Timer
The permanent fix is to stop relying on page loads and drive cron from the operating system instead. Two small edits.
First, tell WordPress to stop spawning cron on page loads by adding one line to wp-config.php, above the line that reads “That is all, stop editing!”:
Second, add a real system cron job that calls the same file every five minutes. This runs whether or not a single visitor shows up.
Why WP-CLI instead of a web request? Calling wp cron event run --due-now skips HTTP entirely, so no plugin, firewall rule, or login wall can block it. It is more reliable than a wget to wp-cron.php, and it fails loudly instead of silently. If you do not have WP-CLI, the wget version works too:
*/5 * * * * wget -q -O - "https://yourdomain.com/wp-cron.php?doing_wp_cron" >/dev/null 2>&1
On managed hosts you often do not need a crontab at all. Kinsta, WP Engine, and similar platforms run a server level cron you enable in the panel, which is the cleanest option because it survives migrations and never depends on your theme or plugins.

When It Is Not wp-cron At All
Cron is the usual culprit, but three other things produce the exact same “missed” symptoms. If next_run_gmt looks correct and the loopback returns 200, check these.
- A timezone mismatch. Go to Settings, General and compare the timezone to the server clock, which is usually UTC. If your site thinks it is in the wrong zone, 9:00 AM is not 9:00 AM, and the post looks late even though cron is fine.
- The post date is already in the past. If you drafted a post days ago with a future date and then edited it, the stored date can slip behind today. WordPress cannot “publish in the future” for a moment that already passed, so it parks the post.
- A stuck
doing_cronlock. A crashed run leaves the lock set. Clear it withwp transient delete doing_cronand the next run proceeds normally.
Also audit anything that blocks the loopback: the “Disable WP-Cron” toggle inside Wordfence or iThemes Security, a Cloudflare rule that challenges wp-cron.php, a maintenance mode or coming soon plugin, and Basic Auth on a staging site. All of them break the page load trigger, and none of them tell you.
Common Mistakes That Keep Schedules Broken
- Setting DISABLE_WP_CRON and forgetting the real cron. This is the big one. You have now disabled the only trigger that existed, and nothing runs at all. Add the system cron in the same sitting.
- Running cron once an hour. Your scheduled posts can now be up to fifty nine minutes late. Five minutes is the sweet spot for a content site.
- Blocking wp-cron.php in a firewall while still relying on the default trigger. Block the public URL only after you move to a system cron.
- Publishing a future dated post manually to “fix” it. You have not fixed the scheduler, so the next post misses too.
- Editing the wrong server. On a load balanced or staging setup, the cron line has to exist on the machine that serves production.
Why This Matters More Once AI Is Scheduling For You
This used to be a niche annoyance. Now it is a silent content killer. AI writing and publishing tools, including agent pipelines that draft, schedule, and publish on a cadence, assume the scheduler works. When cron is unreliable, your pipeline does not error out. It just quietly drops a publish, and you find out a week later when the calendar has gaps.
If you are running an automated workflow, treat cron as infrastructure. Pair a reliable system cron with a monitoring habit, so a missed run is a notification instead of a mystery. Two of our guides go deeper on both halves: writing SEO-optimized posts with AI covers the content side, and automating WordPress maintenance with an AI agent covers the scheduling and reliability side. And if your site still feels slow after you fix cron, these ten WordPress speed fixes are the natural next step.
If your hosting is the bottleneck: Kinsta keeps your site safe with automatic daily backups and one-click restore, so a bad update or a hack is a rollback, not a rebuild. Their support is top tier, and 24/7 live chat is staffed by WordPress engineers who will help fix the actual problem with you, so you are not stuck waiting on your developer to reply. Plans start at $35 per month.
Check out our beginner hosting comparison, five hosts compared on speed, support, and price, so you can pick one without overpaying.
See Our ComparisonThe post was scheduled for a future time, but wp-cron never ran at that moment. wp-cron only checks for due events when someone loads a page, so a low traffic site or a blocked loopback request can leave the event sitting until you see the red Missed schedule label.
Not dependably. wp-cron fires on page loads, so a site with few visitors may check the schedule only a handful of times a day. For anything time sensitive, disable the default trigger and run a real system cron every five minutes instead.
Yes, as long as you replace it. Adding define(‘DISABLE_WP_CRON’, true) to wp-config.php only turns off the unreliable page load trigger. You must add a system cron job that calls wp-cron.php or WP-CLI every five minutes, otherwise nothing will run at all.
Every five minutes is the standard for a content site. It keeps scheduled posts and maintenance tasks on time without adding meaningful server load. Running it hourly makes posts up to fifty nine minutes late.
It can help a little. The default trigger spawns an extra HTTP request on page loads, which adds overhead on busy sites. Moving to a system cron removes that request, and clearing a stuck doing_cron lock can also fix hangs.
Final Thoughts
Scheduled posts that never publish are almost never a WordPress bug. They are wp-cron doing exactly what it was designed to do, which is wait for a page load that a quiet site never provides. Disable the page load trigger, hand the job to a real system cron every five minutes, and the problem disappears for good.
Do the diagnosis first so you know it is cron and not a timezone or a stuck lock. Then make the two edits, confirm with wp cron event run --due-now, and enjoy the fact that your 9:00 AM post now actually publishes at 9:00 AM.

