WordPress

Why Your Scheduled WordPress Posts Keep Missing Their Publish Time

9 min read

A client once called us in a mild panic because a scheduled blog post — set to go live at 9am for a product launch — never actually appeared. No error message, no failed cron job in any log they could find, just… nothing. The post sat there as “Scheduled” for six hours until someone happened to visit the site and, a second later, it quietly published itself.

That’s not a bug. That’s WordPress working exactly as designed, and it trips people up constantly because almost nobody realizes WordPress doesn’t actually have its own cron job. It fakes one, and the difference matters more than you’d think.

If you’ve ever had a scheduled post go out late, a backup silently skip a night, or a cache that never seems to clear on schedule, this is very likely why — and it’s a five-minute fix once you understand what’s actually happening under the hood.


What WP-Cron Actually Is (Hint: Not a Real Cron Job)

Every WordPress install ships with a file called wp-cron.php that’s supposed to handle scheduled tasks — publishing future posts, checking for plugin updates, sending scheduled emails, running any recurring job a plugin registers. On a real Linux server, “cron” means a system-level daemon that wakes up on its own, on a fixed schedule, whether or not anyone’s looking at the machine.

WordPress doesn’t have access to that. It’s just PHP running inside a web request, so it improvised: instead of a real scheduler, WP-Cron piggybacks on your site’s own traffic. Every time someone (or something) loads a page, WordPress checks a hidden loopback request to see whether anything is due to run, and if so, kicks it off in the background.

Every Page Load Is a Wake-Up Call

This is why it’s sometimes called a “pseudo-cron.” No visitors, no wake-up call, no scheduled tasks run — full stop. It doesn’t matter if you told a post to publish at exactly 9:00am. WordPress isn’t checking the clock; it’s checking whether anyone showed up.

For a busy site with steady traffic all day, you’d probably never notice. Someone’s always loading a page, so the check fires constantly and things run close enough to on time that nobody complains. The problem shows up on the sites that don’t have that luxury.

Where This Actually Breaks

Low-traffic sites, staging environments, sites gated behind a login or password, and anything sitting overnight in a quiet timezone all hit the same wall: if nobody visits between midnight and 8am, nothing scheduled for 3am runs at 3am. It runs whenever the first visitor of the day shows up, however much later that turns out to be. We’ve seen this delay a launch post by most of a business day.

WHY IT MATTERS

It’s not just late blog posts. WP-Cron also drives your backup plugin’s nightly run, security scans, cache-clearing jobs, broken-link checks, and any AI feature that runs on a schedule (bulk SEO scans, scheduled content generation, and so on). A quiet site can go days thinking its backups are running fine when the trigger to actually start them never fired.

The Symptoms You’re Probably Blaming on Something Else

Because WP-Cron fails silently — there’s no error, no red banner, nothing in your inbox — the symptoms tend to get misdiagnosed as a plugin bug, a hosting issue, or “WordPress being flaky.” A few we run into constantly:

  • Scheduled posts publish hours late, or occasionally not at all if traffic is thin enough for long enough.
  • Backup plugins report a job “completed” the previous morning when it actually ran at 11pm the night before, because that’s just whenever the first pageview after the scheduled time happened to land.
  • Cache invalidation after a content update lags — you save a post, tell your cache plugin to purge on a schedule, and stale content shows for visitors until traffic triggers the purge.
  • A quiet weekend followed by a Monday traffic spike causes a pile-up: every overdue task from Saturday and Sunday tries to fire on that first Monday-morning page load, all at once, which can genuinely slow that one request down.

None of these are the plugin’s fault. They’re all downstream of the same root cause — the trigger that’s supposed to kick things off never got pulled because nobody visited the site.

How to Check If WP-Cron Is Actually the Problem

Before changing anything, confirm what’s actually scheduled and whether it’s running on time. If you have SSH/WP-CLI access, this is the fastest way to see the truth:

wp cron event list

This prints every scheduled hook, its next run time, and its recurrence (once, hourly, twice daily, daily, or a custom interval a plugin registered). If you see hooks with “next run” times that are well in the past, that’s your smoking gun — WordPress knows those jobs are overdue, it just hasn’t had a visitor to trigger them.

No SSH access? Several free plugins (WP Crontrol is the most common one we install for clients) give you the same event list from wp-admin, plus a button to run any hook manually so you can test that it actually works once triggered.

The Real Fix: Turn Off the Fake Cron, Use a Real One

The good news is the fix has existed since basically forever, and it takes two steps.

Step One: Disable the Pageview-Based Trigger

Add this to wp-config.php, above the line that says “That’s all, stop editing”:

define('DISABLE_WP_CRON', true);

This doesn’t disable scheduling — it just stops WordPress from trying to trigger jobs on every page load. Nothing will run at all until you complete step two.

Step Two: Add a Real System Cron Entry

Most hosts give you cPanel’s Cron Jobs tool, or plain SSH access to edit your crontab directly. Either way you’re aiming for something like this, run every 15 minutes:

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

Fifteen minutes is a sane default for most sites — frequent enough that scheduled posts feel prompt, infrequent enough not to add meaningful load. A high-traffic publisher might want five minutes; a low-traffic brochure site can usually get away with thirty. curl works exactly as well as wget here if that’s what your host provides. The ?doing_wp_cron parameter isn’t strictly required by WordPress’s own docs (a plain call to wp-cron.php works too) but it’s a common, harmless convention plenty of hosting tutorials include, so leaving it in doesn’t hurt anything.

Once this is in place, WordPress gets a genuinely reliable, clock-driven trigger instead of one that depends entirely on someone showing up. Scheduled posts publish close to on time, backups run when they’re supposed to, and cache purges happen without waiting on a random visitor.

Cleaning Up Orphaned Cron Hooks

Once you’re actually looking at wp cron event list, you’ll often find hooks for plugins that aren’t installed anymore — jetpack_clean_nonces, wordfence_daily_cron, that sort of thing, sitting there scheduled with nothing left to run them. That’s not evidence of anything malicious; it’s just leftover scheduling data a plugin never cleaned up on uninstall. WordPress happily wakes up, finds nothing hooked to the event, and does nothing.

We ran into this during a security review a while back: a client had spotted an unfamiliar-looking cron hook and, understandably, assumed the worst. Digging into it, the callback hash attached to the event was the MD5 of an empty string — meaning literally no PHP function was wired up to it at all. It was dead weight from a security plugin removed months earlier, not a backdoor. Worth checking before you panic, but also worth cleaning up, since an event with hundreds of orphaned instances stacking up in the options table is exactly the kind of database bloat that slows a site down over time.

SET IT AND FORGET IT

The two-step fix above is a one-time, five-minute change. Once it’s live, you never have to think about it again — no plugin to maintain, no dashboard to check, just a boring line in a crontab quietly doing its job every 15 minutes. It’s the kind of fix that’s easy to put off because nothing looks broken, right up until a launch post sits unpublished all morning.

Quick Reference: WP-Cron’s Built-In Schedules

Plugins register recurring jobs against a handful of intervals WordPress ships by default. Knowing these helps when you’re reading through wp cron event list and trying to figure out how urgent a given delayed hook actually is:

DEFAULT INTERVALS

hourly — every 3,600 seconds. Common for update checks and lightweight maintenance.

twicedaily — every 12 hours. A lot of caching and update-check plugins default here.

daily — every 24 hours. Backups and full-site scans usually land in this bucket.

weekly — every 7 days (added in WordPress 5.4). Less commonly used, but available for anything that doesn’t need to run more often. Plugins can also register their own custom interval via the cron_schedules filter, which is why you’ll sometimes see numbers that don’t match any of these four defaults.

Should You Ever Just Leave WP-Cron Alone?

Honestly, yes, sometimes. If you’re running a personal blog with steady daily traffic and you’ve never noticed a scheduling problem, the default behavior is probably fine — the whole point of this fix is solving a problem you’ve actually observed, not adding one more moving part to every site by default. Where it stops being optional is anything client-facing, anything with a hard publish deadline, or any site you’re not confident gets consistent traffic around the clock. For those, the system-cron switch is cheap insurance against a problem that’s otherwise invisible until it costs someone a launch.

Frequently Asked Questions

WP-Cron is WordPress’s built-in system for handling scheduled tasks, like publishing future posts or running backups. Unlike a real system cron job, which is triggered by the server’s own clock regardless of traffic, WP-Cron only checks for due tasks when someone loads a page on your site. No visitors means no scheduled tasks run, no matter what time you set.

Because WP-Cron is triggered by page visits, not the clock. If a post is scheduled for 3am and nobody visits your site until 8am, the post publishes at 8am, whenever the first pageview happens to occur. Sites with steady round-the-clock traffic rarely notice this; low-traffic or gated sites feel it constantly.

Don’t disable it without replacing it. Set DISABLE_WP_CRON to true in wp-config.php to stop the pageview-based trigger, but pair it with a real system cron entry that hits wp-cron.php on a fixed schedule (every 5-15 minutes is typical). Disabling it alone with no replacement means scheduled tasks stop running altogether.

Most hosts offer a Cron Jobs tool in cPanel, or direct crontab access over SSH. Add an entry that runs on your chosen interval and calls wget -q -O - https://yoursite.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1 (curl works the same way). Combine this with DISABLE_WP_CRON set to true in wp-config.php so the pageview trigger stops competing with it.

Every 15 minutes is a reasonable default for most sites. High-traffic or publishing-heavy sites can drop to every 5 minutes for snappier scheduling; a low-traffic brochure site can usually stretch to every 30 minutes without anyone noticing the difference.

Usually not. Plugins frequently leave scheduled cron events behind after being uninstalled, since cleaning up your own scheduled hooks isn’t something every plugin bothers to do. Check whether a real function is actually attached to the hook before assuming it’s malicious — an orphaned event with nothing hooked to it just sits there harmlessly until you clear it out.

A five-minute config change now beats explaining to a client why their launch post sat unpublished all morning.

Built by amplifi.studio — see also The Hidden Weight Slowing Your Site: A WordPress Database Cleanup Guide.