Missed schedule: fix WordPress scheduled posts that did not publish

WordPress shows 'Missed schedule' when the publish time passed but the cron runner never fired. The post is not lost. This article gets the stuck post live in the next minute and fixes the underlying cause so the next one publishes on time.

You scheduled a post for 09:00. It is now 10:30, the post is not on the front page, and in the Posts list it shows the red label "Missed schedule" instead of "Scheduled". Sometimes the date in the list still shows the original publish time. Sometimes the post sits in the future tense ("Missed schedule for 09:00") even though that time has long passed.

What "Missed schedule" actually means

WordPress did not lose your post. The content is intact. What failed is the runner that was supposed to flip the post from future status to publish status at the scheduled moment.

When you click "Schedule" in the editor, WordPress saves the post with post_status = future and registers a one-time cron event called publish_future_post for that exact timestamp. The event is tied to your post by ID through _future_post_hook(), which calls wp_schedule_single_event() to register the publish moment. When that moment arrives and the cron runner fires, check_and_publish_future_post() (in core since WordPress 2.5.0) calls wp_publish_post() to transition the post to publish and the content goes live.

If that whole sequence does not run on time, the post stays in future status. The wp-admin Posts list spots that the GMT publish time is in the past while the status is still future, and shows the red "Missed schedule" label as a UI signal. There is no separate "missed_schedule" status in the database. It is the same future status, with the wall clock having moved past the target time.

The root cause is almost always WP-Cron not running when it was supposed to. WP-Cron is a PHP routine that fires on page loads, not a real cron daemon, and it has well-known reasons to skip its window.

Common causes, ordered by likelihood

After working through hundreds of these on managed WordPress sites, the order is consistent. The first three cover the vast majority.

  1. No traffic on the site at the publish moment. WP-Cron only runs when a visitor (human or bot) hits a non-cached PHP request. A scheduled post on a low-traffic blog at 03:00 on a Sunday will sit in future until the next visitor arrives.
  2. Aggressive full-page caching swallowing the trigger. A reverse-proxy cache, an nginx fastcgi_cache, LSCache, WP Rocket disk cache, or Cloudflare Cache Reserve serves visitor requests without ever bootstrapping PHP. WordPress never starts up, so spawn_cron() never runs. The site looks busy in analytics while WP-Cron quietly stops.
  3. DISABLE_WP_CRON is set in wp-config.php without a working external runner. Many managed hosts (WP Engine, Kinsta, Cloudways, SpinupWP) ship with define( 'DISABLE_WP_CRON', true ); and bolt their own runner on top. If the site was migrated, the runner was removed, or someone disabled WP-Cron "for performance" without adding a system cron, every scheduled event silently stops.
  4. A timezone mismatch between WordPress and the server. WordPress stores scheduled times in two columns: post_date (in your site's display timezone) and post_date_gmt (in UTC). The cron runner compares post_date_gmt to the server's current GMT time. If a plugin or theme calls date_default_timezone_set() to a non-UTC value, the math gets doubled up or cancelled out and posts get scheduled for the wrong moment in the past or far into the future.
  5. The PHP memory limit was hit during the cron run. A heavy plugin loaded inside the wp-cron.php request can blow past the PHP memory limit. The cron run dies with Allowed memory size exhausted before reaching check_and_publish_future_post(). The post stays in future.
  6. A plugin throws a fatal during the publish hook. A plugin that hooks into transition_post_status, publish_post, or one of the post-save actions can fatal during publishing. The cron run aborts. The post status is not transitioned. WordPress shows "There has been a critical error" only if you trigger the publish manually; the cron path swallows it silently.

The diagnosis section below tells you which one applies before you start changing things.

Quick relief: get this post published right now

Before debugging, get the stuck post live. Pick whichever path you have access to.

Option A: re-trigger from wp-admin (no plugin, no shell)

  1. Open the Posts list in wp-admin and find the post with the red "Missed schedule" label.
  2. Click Quick Edit.
  3. Change the Date to a moment in the past (one minute ago is enough), or change Status to Published directly.
  4. Click Update.

The post goes live within a second. This works because saving with a past date and publish status transitions it directly through wp_publish_post(), bypassing the cron event entirely. You will know it worked when the red label disappears from the Posts list and the post appears on your site's front page.

Option B: install Missed Scheduled Posts Publisher (covers future occurrences too)

If you do not have shell access and you want a quick safety net while you fix the root cause, install Missed Scheduled Posts Publisher by WPBeginner (version 2.1.1, 60,000+ active installs). It registers its own WP-Cron event that runs every fifteen minutes and re-checks for future-status posts whose publish time is in the past, then publishes them.

Be honest about what this plugin is. It treats the symptom, not the cause. The plugin still depends on WP-Cron running at all, and its 15-minute interval means a post scheduled for 09:00 may publish at 09:15 instead of 09:00. If the underlying cause is DISABLE_WP_CRON without a runner, this plugin will not help either, because its scheduled event will not run for the same reason yours did not. Treat it as a stopgap, not a fix.

Option C: WP-CLI (if you have SSH access)

From the site root over SSH:

# Run any cron events that are overdue, including publish_future_post
wp cron event run --due-now

The command bypasses HTTP entirely, runs the WordPress bootstrap in PHP CLI context, and fires every overdue event. If the only thing wrong was that nothing had triggered WP-Cron yet, this publishes the missed post immediately. The full reference is in the WP-CLI cron docs.

To target only the publishing event for a specific post:

# Find the post ID
wp post list --post_status=future --fields=ID,post_title,post_date

# Trigger publish_future_post for that ID
wp cron event run publish_future_post

You will know it worked when wp post list --post_status=future no longer returns the post, and wp post get <ID> --field=post_status returns publish.

Diagnosing which cause applies

Once the stuck post is live, work out why it stuck so the next post does not.

Step 1: check Site Health for missed cron events

In wp-admin, open Tools, then Site Health, then Status. WordPress runs a "Scheduled events" check that looks at every registered cron event and flags ones whose scheduled time has passed by more than a hard-coded threshold: 5 minutes by default, extended to 1 hour when DISABLE_WP_CRON is true. If Site Health shows a recommendation labeled "A scheduled event has failed" or "A scheduled event is late", you have direct confirmation that the runner is not firing.

The red flags to act on are:

  • "A scheduled event has failed" with publish_future_post in the list of stuck events
  • "A scheduled event is late"
  • A failed loopback test (under "Info, then Scheduled events"), which means spawn_cron() cannot reach wp-cron.php from the server back to itself

Step 2: check the timezone settings

In wp-admin, go to Settings, then General. Note the Timezone setting (for example, "Amsterdam" or "UTC+1"). WordPress's wp_timezone(), introduced in WordPress 5.3.0, reads this setting and is used by the cron pipeline to translate scheduled times to UTC.

Compare it to the server's PHP timezone. Add a temporary diagnostic file at the site root called tz-check.php:

<?php
echo 'PHP date_default_timezone_get(): ' . date_default_timezone_get() . "\n";
echo 'PHP date(): ' . date('Y-m-d H:i:s T') . "\n";

Visit https://yoursite.nl/tz-check.php once. Expected output for a healthy site:

PHP date_default_timezone_get(): UTC
PHP date(): 2026-04-24 13:42:11 UTC

Delete the file when you are done. If the PHP timezone is anything other than UTC, a plugin or theme has called date_default_timezone_set() somewhere, and that is enough to make scheduled posts publish at the wrong moment. WP Crontrol's PHP default timezone note calls this out as the root cause and recommends finding and removing the offending call.

Step 3: check whether WP-Cron is reachable

If you have wp-admin only, the Site Health "Scheduled events" Info entry is the answer.

If you have SSH:

# This is the authoritative check
wp cron test

wp cron test returns a one-line pass or fail. A failure prints the exact cURL error the loopback hit, which maps directly to the WP Crontrol cURL error table (cURL 6 is DNS, cURL 7 is connection refused, cURL 28 is timeout, cURL 35 is TLS handshake, HTTP 401/403 is BasicAuth or a security plugin, HTTP 404 means wp-cron.php is missing or blocked).

Step 4: check for DISABLE_WP_CRON

Open wp-config.php through your hosting panel's file manager (or download it via SFTP) and search for the string DISABLE_WP_CRON. If you find:

define( 'DISABLE_WP_CRON', true );

then WP-Cron is intentionally off and something else must be running the events. Ask your hosting provider whether they run a managed cron job for your site and at what interval. If the answer is "no" or "we do not know", the missed schedules are because nothing is running WP-Cron.

Step 5: check the debug log for cron-time fatals

Enable the WordPress debug log (edit wp-config.php via your hosting panel's file manager and add define( 'WP_DEBUG', true ); and define( 'WP_DEBUG_LOG', true );). Wait for the next missed schedule. Then open wp-content/debug.log through the file manager. Look for entries dated around the missed publish time mentioning Allowed memory size, Fatal error, Uncaught Error, or any plugin file path inside a stack trace. A fatal in the cron context will show up here even when the front-end never errored.

Cause-specific fixes

Fix 1: low traffic or full-page caching

Replace WP-Cron with a system cron that fires every minute regardless of visitors. Two paths, depending on access.

cPanel-style hosting panels (no SSH). In cPanel, open Cron Jobs, set the schedule to "Every minute" (or "Once per five minutes" for a lighter load), and use:

wget -q -O /dev/null "https://yoursite.nl/wp-cron.php?doing_wp_cron" > /dev/null 2>&1

Hosting with SSH and WP-CLI. Edit the site user's crontab with crontab -e and add:

* * * * * /usr/local/bin/wp cron event run --due-now --path=/var/www/yoursite.nl/htdocs --quiet > /dev/null 2>&1

Then add to wp-config.php (via the file manager or SFTP), above the "/* That's all, stop editing! */" comment:

// Disable the page-load trigger; the system cron now drives WP-Cron.
define( 'DISABLE_WP_CRON', true );

The full reasoning, including why every minute and why WP-CLI beats wget, is in WordPress wp-cron: why it fails and how to replace it.

You will know it worked when you schedule a test post for two minutes from now, walk away, and the post appears at the expected time on the front page.

Fix 2: DISABLE_WP_CRON without a runner

You have three options:

  • Add a system cron as in Fix 1 above. This is the right fix for a self-hosted setup.
  • Re-enable WP-Cron by removing the define( 'DISABLE_WP_CRON', true ); line from wp-config.php. Use only if your traffic is consistent enough that page loads will fire WP-Cron reliably.
  • Ask your managed host whether their built-in cron runner is healthy. WP Engine, Kinsta, Cloudways, and SpinupWP each run their own. If their runner is broken, file a support ticket. Do not add a second cron on top of theirs without coordinating: two runners racing the same events cause lock contention.

You will know it worked when wp cron event list --due-now (or the WP Crontrol plugin's events table) returns no overdue events two minutes after a scheduled job's expected time.

Fix 3: timezone mismatch

If your tz-check.php showed a non-UTC PHP timezone, find and remove the call. Search the codebase:

grep -rn "date_default_timezone_set" wp-content/

Common offenders are old caching plugins, custom themes that included a "fix timezone" snippet from a tutorial in 2014, and a few outdated date-handling libraries. Remove the call (or update the plugin to a current version), then re-run tz-check.php. Expected output: UTC.

If the WordPress Settings, then General, then Timezone value is wrong (for example, set to "UTC" when your site is meant to be on Europe/Amsterdam), correct it. Future schedules will use the new timezone. Existing scheduled posts have already had their post_date_gmt calculated against the old setting, so reschedule them by hand from the post editor.

You will know it worked when a fresh test post scheduled for two minutes from now publishes within the same minute as expected.

Fix 4: PHP memory limit hit during cron

Raise the limit so the cron run can complete. Edit wp-config.php via the file manager and add, above the "/* That's all, stop editing! */" comment:

define( 'WP_MEMORY_LIMIT', '256M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' );

WP_MEMORY_LIMIT controls the front-end limit, WP_MAX_MEMORY_LIMIT covers the admin and cron context. Some hosts cap these at the PHP memory_limit in php.ini, which you may need to raise via your hosting panel's PHP settings. The full troubleshooting path for memory-related fatals is in Fatal error: Allowed memory size of N bytes exhausted.

You will know it worked when the next scheduled post publishes on time and the debug log shows no Allowed memory size entries around the publish moment.

Fix 5: plugin fatal in the publish hook

Identify the plugin from the stack trace in debug.log. Then deactivate it from wp-admin, schedule a test post, and confirm publishing works. If the plugin is essential, contact its support with the stack trace and your PHP version. If it is replaceable, swap it. Do not leave a plugin that fatals during cron in place: it does not just break scheduled posts, it breaks every cron-driven feature on the site (backup runs, sitemap rebuilds, WooCommerce order emails).

What "Missed schedule" is NOT

Three things often get blamed for missed schedules and almost never are.

  • It is not a sign your server was down. A missed schedule means the cron runner did not fire on time. The webserver can be handling thousands of requests per minute while publish_future_post quietly sits in the queue. If the site itself was unreachable, you would see 502 Bad Gateway or 503 Service Unavailable in your monitoring, not "Missed schedule" on a single post.
  • It is not the WordPress timezone setting on its own. Misconfiguring Settings, then General, then Timezone changes when posts publish (an hour or two off), it does not normally cause a publish_future_post event to be missed entirely. The combination of WordPress settings timezone plus a date_default_timezone_set() call in PHP is what causes the math to break. Both have to be considered.
  • It is not solved by Missed Scheduled Posts Publisher alone. That plugin re-checks every 15 minutes, but it relies on WP-Cron running. If WP-Cron is dead, the plugin's check is also dead. It is a useful safety net once WP-Cron is healthy, not a substitute for fixing the cron runner.

When to escalate

If you have applied the cause-specific fix and posts still miss their schedule, collect this before opening a ticket with your host or asking me to investigate:

  • WordPress version, PHP version, MySQL or MariaDB version
  • Active theme name and version
  • A list of active plugins (export from wp plugin list --status=active --fields=name,version if you have WP-CLI, or screenshot the Plugins screen)
  • The output of wp cron test (or, without SSH, the text under wp-admin Tools, then Site Health, then Info, then Scheduled events)
  • The output of wp cron event list --due-now (or screenshot of the WP Crontrol "Cron Events" page sorted by next-run time)
  • The relevant wp-content/debug.log entries from around the missed publish moment
  • Hosting provider name and tier, plus whether DISABLE_WP_CRON is set in wp-config.php
  • Confirmation of which fixes you have already tried

That set of evidence is enough for any competent host or freelancer to find the problem in one pass instead of three.

Preventing it from happening again

Once you have a working setup, lock it down so it stays working:

  • Run a system cron, not page-load WP-Cron. Even on a site with traffic, the system cron is more reliable and removes WP-Cron from your visitors' TTFB budget.
  • Monitor publish_future_post once a month. Open WP Crontrol or run wp cron event list and check that no events are stuck in the past.
  • Keep the WordPress timezone aligned with your editorial calendar's timezone, and the PHP timezone at UTC. Do not let a plugin override date_default_timezone_set(). If a plugin requires it, replace the plugin.
  • Do not stack runners. One reliable cron runner is healthier than two competing ones.
  • Test your scheduling pipeline after every host migration or major WordPress update. Schedule a throwaway draft for two minutes from now and confirm it publishes. The check takes one minute and catches the silent breakage that DISABLE_WP_CRON migrations and aggressive new caching plugins introduce.

If you treat scheduled posts as a critical pipeline (newsletters, product launches, time-bound campaigns), a system cron plus a monthly five-minute audit is the level of attention they deserve. Anything less and you are betting on visitor traffic to publish your work, which is the bet that produced the "Missed schedule" label in the first place.

Want this to stop being your problem?

If outages or errors keep repeating, the fix is often consistency: updates, backups and monitoring that don't get skipped.

See WordPress maintenance

Search this site

Start typing to search, or browse the knowledge base and blog.