Initial impact: posts stuck in the future
future status after their publish time has passed, and the posts list shows "Missed schedule" next to them. Look a little wider and you usually find other quiet failures with the same root: scheduled backups that did not run, plugin cleanup jobs that never fired, WooCommerce or newsletter tasks that lag behind, and update checks that seem stale.
A detail that confuses site owners is that the post often publishes itself the moment they log in to check on it. From their side it looks random. From the server side it is a strong clue, and we will come back to it.
For support staff, this ticket tends to arrive as "your server missed my post". The useful first move is not to look at uptime graphs. It is to ask what was supposed to trigger the publish, and whether that trigger actually happened.
Investigation: checking the scheduler before blaming plugins
cron daemon. It is a list of events stored in the database (in the cron option in wp_options), each with a timestamp and a hook name. When you schedule a post, WordPress adds a single event for the publish_future_post hook at the post's publish time.
So the investigation has three questions. Is the event in the queue? Is WP-Cron allowed to run? And can WordPress actually start it? If you have shell access and WP-CLI, these read-only checks answer most of that in a minute:
BASH
# Is WP-Cron disabled in wp-config.php? (errors if the constant is not defined)
wp config get DISABLE_WP_CRON
# Which posts are still stuck as scheduled?
wp post list --post_status=future --fields=ID,post_title,post_date
# What is queued, and how overdue is it?
wp cron event list --fields=hook,next_run_relative
# Can WordPress spawn WP-Cron through a loopback request?
wp cron testpublish_future_post entries marked as overdue, the queue is fine. The events exist; nobody is running them. That rules out the common theory that a plugin deleted the schedule, and it points at the trigger.
The second source of evidence is the access log for the domain. Every time WP-Cron is spawned, WordPress makes a POST request to /wp-cron.php on its own site. Searching the domain's access log for wp-cron.php tells you how often that has actually happened. Where those logs live depends on the control panel and web server, so check your panel's documentation for the per-domain log path. If you find long gaps with no wp-cron.php requests at all, you have found the window in which the post was missed.
The discovery: WordPress only checks the clock when someone knocks
wp_cron() early in the request. It looks at the queue, and if something is due, it fires a non-blocking request to wp-cron.php to run those events. A transient called doing_cron acts as a lock so that many visitors at once do not start many runs; how long that lock lasts is controlled by WP_CRON_LOCK_TIMEOUT.
In other words, WordPress only checks the clock when a PHP request reaches it. If nobody visits, nothing runs. That is the whole reason quiet sites are affected and busy sites rarely are. A post scheduled for 8:00 on a site with no visitors until 13:00 will publish at about 13:00, and until then it is shown as missed.
Three things make this worse, and the investigation above tells you which one applies.
First, full-page caching. When LiteSpeed Cache, an Nginx FastCGI cache, Varnish, or a CDN serves a cached HTML page, the request never reaches PHP. The visitor gets the page, but wp_cron() never runs. A site can have steady traffic in the access log and still miss schedules, because almost all of that traffic is answered by the cache. This is the same split between cached and uncached requests covered in Why WordPress Admin Is Slow Even When the Frontend Loads Fast, seen from the scheduling side.
Second, DISABLE_WP_CRON set to true in wp-config.php with no replacement. This constant stops WordPress from spawning WP-Cron on page loads. It is meant to be paired with a real system cron job. Sites that were migrated, copied from a template, or tuned by a performance guide sometimes keep the constant and lose the cron job. In that case nothing ever runs the queue, no matter how much traffic arrives.
Third, a broken loopback. Even when wp_cron() decides to spawn, the request to wp-cron.php can fail: the server cannot resolve its own domain, a firewall or security plugin blocks the request, basic auth protects the site, or the SSL certificate the loopback hits is invalid. wp cron test and the Site Health screen in the dashboard both report loopback problems.
This also explains the clue from earlier. Dashboard pages are not served from page cache, so when the owner logs in to check, their own request runs wp_cron(), the overdue event fires, and the post appears. The site was waiting for an uncached visit, and the owner provided one.
Temporary mitigation
wp cron event run --due-now runs every overdue event once, including pending publish_future_post events. After it finishes, rerun wp post list --post_status=future and the posts that were past their time should be gone from the list.
Without shell access, loading any admin page usually has the same effect, as long as DISABLE_WP_CRON is not set and the loopback works. Publishing the post manually from the editor also works, though it does not touch the other overdue tasks.
Plugins that "fix missed schedules" exist, and they generally work by checking for overdue posts on page load. On a cached, quiet site they suffer from the same trigger problem, so treat them as a patch, not a fix.
Architectural fix: give WP-Cron a real clock
wp-config.php, above the line that says to stop editing:
PHP
// Stop spawning WP-Cron on page loads; a system cron job runs it instead.
define( 'DISABLE_WP_CRON', true );crontab -e as the site's user or through the control panel's cron jobs page. Using WP-CLI avoids HTTP, caches, and loopback issues entirely, because it runs PHP directly:
TEXT
*/5 * * * * cd /home/USERNAME/public_html && /usr/local/bin/wp cron event run --due-now >/dev/null 2>&1wp on your server (which wp will tell you). If WP-CLI is not available, a common alternative is requesting https://example.com/wp-cron.php?doing_wp_cron with wget or curl on the same schedule. That works, but it goes back through the web server, so a misconfigured cache rule or a broken certificate can still get in the way.
The order matters. If you set DISABLE_WP_CRON first and forget the cron job, you have created the second failure mode on purpose. Add the cron job, confirm with wp cron event list that events move forward over the next interval, and only then consider the change done. Run the job as the site's own user, not root, so files created by scheduled tasks keep the right ownership.
Five minutes is a reasonable default interval for most sites. Posts will publish within that window rather than on the exact minute, which is usually fine. Sites with time-sensitive jobs can run it every minute; servers hosting many sites may prefer to spread the jobs out so they do not all start at once.
When "Missed schedule" shows up, treat it as a trigger problem, not a server outage. Check whether the event is in the queue, whether DISABLE_WP_CRON is set, whether page cache is swallowing every request, and whether the loopback works. On any site that is quiet, heavily cached, or simply important, a real system cron job running WP-Cron is the setup that removes the guesswork.