WordPress has no clock. When a post is scheduled for 2 p.m., no process waits until 2 p.m. to publish it. WordPress checks, on each visit, whether a task is due. If nobody visits the site before 5 p.m., the post waits until 5 p.m.: that is the official documentation’s own example, applied here to a post.
This mechanism, WP-Cron, carries far more than scheduled posts: update checks, automatic updates, emptying the trash and expired transients, many plugins’ backups, a store’s subscription renewals. Since WordPress 7.1, it also carries the daily cleanup of personal data requests.
This guide explains how WP-Cron really fires, why it misses on small sites and weighs on large ones, how to replace it with a real system cron, how WooCommerce’s job queue fits in, and how to debug and monitor scheduled tasks.
How WP-Cron really fires
A check on every request, a lock against duplicates
On every request that reaches PHP, WordPress looks at the list of scheduled tasks, stored in a single autoloaded database option. If a task is due, it starts execution, but a lock, set by the WP_CRON_LOCK_TIMEOUT constant to sixty seconds by default, blocks a new spawn while the previous one is running, for up to those sixty seconds.
The loopback request to wp-cron.php
So the visitor does not have to wait, WordPress does not run the tasks within their request. It sends an HTTP request to its own wp-cron.php file, with a 0.01-second timeout and in non-blocking mode, then keeps rendering the page. That second process runs the due tasks.
This loopback assumes the server can call itself. A firewall, HTTP basic authentication, a DNS or certificate problem is enough to block it, and tasks stop running.
What depends on WP-Cron
WordPress core schedules a set of tasks itself: checking for core, plugin and theme updates twice a day, daily deletion of expired transients, old auto-drafts and trash, the weekly Site Health check, and since WordPress 7.1 the daily cleanup of personal data requests. Automatic updates depend on it directly.
Plugins add their own tasks: backups, email sends, syncs, cleanups. A WP-Cron that stops running therefore means a site that no longer updates, backs up or cleans itself, with no visible error message.
What the page cache changes
A page served by a page cache or a CDN never reaches PHP, and therefore never reaches WP-Cron. A very well cached site can thus receive thousands of visits without a single one triggering scheduled tasks.
The real case: the “non-blocking” request that blocked
The story of moving WP-Cron in WordPress 6.9, in the fall of 2025, shows what visit-triggered scheduling costs. It is documented in core commits, a performance team note and an issue on WordPress’s HTTP library.
A 0.01-second timeout that lasted a second
The loopback request is supposed to never make the visitor wait. Yet in August 2023, a developer reported on the repository of the Requests library, which WordPress uses for its HTTP calls, that non-blocking mode did not always work: his script stayed suspended until the response came back. In October 2026, that issue is still open.
Core’s code now acknowledges it in its comments: the request function does not always respect the timeout and blocking parameters, and a 0.01-second timeout may end up taking one second. As long as the spawn happened before the page was sent, that delay held back the first byte the visitor received.
The move
On October 12, 2025, Peter Wilson, a core committer, landed the change prepared by Weston Ruter: the WP-Cron spawn moved to the very end of the request, once the page has been sent. The commit message sums up the problem: the loopback request was meant to be non-blocking, but was not always, and increased response time.
On November 18, Weston Ruter, from the performance team, presented the change in the WordPress 6.9 guide: a possible one-second reduction in time to first byte, for requests that spawn WP-Cron. We found no independent measurement of that gain.
A regression before release
The move broke one particular setup: sites using the ALTERNATE_WP_CRON constant, a fallback method that redirects the visitor to run tasks within their own request. On November 27, 2025, five days before release, Weston Ruter restored the old behavior for those sites only. WordPress 6.9 shipped on December 2, 2025 with both changes.
What ALTERNATE_WP_CRON does
This fallback mode only applies to standard read requests, excluding AJAX and XML-RPC. Instead of sending a loopback request, WordPress redirects the visitor to the same address with a spawn parameter, then runs the tasks in the original request, the one the browser has just left. The documentation presents it as a fix for scheduled posts that fail to publish. It is usually turned on when the loopback request is blocked. The visitor pays the price here too, with an extra redirect on every spawn.
What to take away
Visit-triggered scheduling is paid for by a real visitor: a 0.01-second request can cost a second. WordPress 6.9 moved that cost after the page is sent, without removing it: the spawn still runs inside a visitor’s PHP process, and the HTTP library problem is not fixed. Fallback modes such as ALTERNATE_WP_CRON remain fragile special cases.
With a system cron and WP-Cron disabled on visits, none of these questions arise: WordPress no longer even tries to spawn tasks during visitors’ requests. If your site’s response time concerns you, our guide to Core Web Vitals fixes details the other levers.
Why WP-Cron misses on small sites and weighs on large ones
Low traffic: the task waits for a visitor
On a little-visited site, or one almost entirely served by the cache, tasks wait for the next request that reaches PHP. A scheduled post looks late, a nightly backup starts in the morning, a subscription renewal waits for a customer to log in.
High traffic: a busy process, a single queue
On a very busy site, WP-Cron spawns often, and each spawn ties up a PHP process on the server for as long as the tasks run. Tasks run one after another: a long task delays all the following ones, and if it exceeds the lock timeout, another process can take over.
Memory and execution time
Since WordPress 6.3, wp-cron.php raises PHP’s memory limit to the value meant for heavy tasks, 256 MB by default. That increase does not apply to tasks run by WP-CLI or by a mechanism that bypasses this file. On the command line, however, PHP sets no maximum execution time by default, which suits long tasks better.
The catch-ups that never happen
A missed recurring task is not replayed. If a daily task could not run for three days, it runs only once when things recover, then WordPress calculates the next due date. For a cleanup, that does not matter. For a process that counts on every run, executions are lost.

Scheduling properly: intervals and wp_schedule_event
Core intervals and custom intervals
WordPress provides four intervals: hourly, twice daily, daily and weekly. A plugin can add more with the cron_schedules filter, giving a duration in seconds and a label.
add_filter( 'cron_schedules', function ( $schedules ) {
$schedules['every_ten_minutes'] = array(
'interval' => 600,
'display' => 'Every ten minutes',
);
return $schedules;
} );
A very short interval guarantees nothing: the one-minute lock and the pace of visits, or of the system cron, set the real frequency.
Avoiding duplicates and cleaning up
Before scheduling a recurring task, a plugin must check that it does not already exist, or it will pile up on every load. The documentation offers this pattern, and reminds you to remove the task when the plugin is deactivated.
if ( ! wp_next_scheduled( 'my_plugin_hourly_job' ) ) {
wp_schedule_event( time(), 'hourly', 'my_plugin_hourly_job' );
}
register_deactivation_hook( __FILE__, function () {
wp_clear_scheduled_hook( 'my_plugin_hourly_job' );
} );
For a single event, WordPress ignores a new scheduling of the same event within ten minutes of an existing one, unless its arguments differ. A task that “will not schedule” often comes from this.
A task that runs too long
A long task poses a particular problem. If it exceeds the lock duration, another spawn can take over while it is still running; core’s code then stops the first process’s loop. Heavy processing therefore benefits from being split into small batches, each scheduled separately, or handed to a queue such as Action Scheduler, built for that work.
Switching to a real system cron
DISABLE_WP_CRON: what the constant does, and does not do
The DISABLE_WP_CRON constant, in wp-config.php, removes the visit-triggered spawn. It does not disable tasks: wp-cron.php keeps running them when called. The file’s own comment says so: the two are independent.
define( 'DISABLE_WP_CRON', true );
Setting this constant without putting another trigger in place is the most common mistake: nothing runs anymore, silently, no scheduled posts, no automatic updates, no renewals.
Calling wp-cron.php by URL, or running WP-CLI
The documentation suggests calling wp-cron.php from the system scheduler. A crontab line that does so every fifteen minutes looks like this.
*/15 * * * * wget -q -O - https://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1
The other method runs due tasks with WP-CLI, without going through HTTP. The SpinupWP server control panel wraps it in flock, which prevents two simultaneous runs.
*/5 * * * * cd /path/to/wordpress && flock -n ~/.wp_cron.lock /usr/local/bin/wp cron event run --due-now --quiet
Each method has its strengths. The URL call keeps core’s lock and the memory increase meant for tasks, but depends on the network and the site’s authentication.
WP-CLI avoids all loopback problems and has no default time limit on the command line, but the current stable release does not respect core’s lock: hence the value of flock, or of using a single method. Do not run WP-CLI as root: its documentation strongly advises against it. Our selection of essential WP-CLI commands details its setup and uses.
Checking that the system cron runs
Once it is in place, list the tasks and their next due date. A task whose due date passed long ago signals a trigger that is not working.
wp cron event list --fields=hook,next_run_relative
Multisite and managed hosts
On Multisite, each site has its own task list: you need one call per site, for example by looping over the site list with WP-CLI. Many managed hosts offer their own trigger, each at its own pace. Kinsta runs a server cron on every site every fifteen minutes, and you then disable WP-Cron yourself.
WP Engine offers an Alternate Cron you switch on in its portal, which checks for due tasks every minute. Pantheon runs them hourly, and WordPress VIP runs its own runner every thirty seconds. Ask your host what it does before adding your own cron.
Two triggers in parallel do not make the site more reliable: they sometimes run the same tasks twice. Each host also has its constraints: Pantheon requires external calls to leave the spawn parameter empty, and WP Engine warns that its alternate cron does not work behind custom HTTP authentication. Pick a single method, and document it.
Action Scheduler, WooCommerce’s job queue
How it relies on WP-Cron
WooCommerce and many plugins hand their background jobs to Action Scheduler, a queue that stores each job in its own tables: renewals, emails, webhooks, analytics imports. Its documentation states it does not depend on WP-Cron, yet WP-Cron is what starts it by default, at most once a minute, in batches of twenty-five jobs for thirty seconds.
If WP-Cron stops, the queue only runs when someone loads the admin, which on a store nobody logs into means almost never. The WooCommerce Subscriptions documentation even treats several jobs more than a day overdue as a possible sign of a WP-Cron problem.
Running it with WP-CLI on a busy store
For a store that processes many jobs, the Action Scheduler documentation recommends WP-CLI, far better suited than the default runner. A dedicated cron line runs the queue regularly.
wp action-scheduler run --batch-size=100
A small official plugin then lets you disable the default runner, with a clear warning: without WP-CLI or another method, no jobs will be processed anymore.
WooCommerce subscriptions
WooCommerce Subscriptions relies on Action Scheduler for recurring payments, trial ends, payment retries and expirations. Its documentation states that, without WP-Cron, the queue can then only be processed by admin requests. A health check, in the WooCommerce status screens, summarizes overdue subscription jobs. For a subscription store, a dedicated system cron is essential.
Retention and growing tables
Completed or canceled jobs are purged after thirty-one days; since version 4.0, shipped with WooCommerce 11.0, failed jobs are purged after three months. A queue that does not run, or that a plugin floods, still inflates the database, as our guide to WordPress database optimization recounts.
Posts showing “Missed schedule”
What scheduled publishing does
Scheduling a post creates a single event at the planned publication date. When it runs, WordPress publishes the post if it is still scheduled and its time has come; if the event fires too early, it reschedules itself. If WP-Cron does not run, the posts list shows a missed schedule message, flagged in red.
Catching up without triggering everything
To publish late posts, WP-CLI runs the due tasks. Beware of a trap: without the --due-now option, the command applied to a task name runs every event with that name, including those planned for the future. Scheduled publishing protects itself, since the event reschedules if its time has not come, but other tasks would run early.
wp cron event run --due-now
Plugins such as MWW Scheduled Post Trigger or Missed Scheduled Posts Publisher republish missed posts. These plugins depend on visits themselves and only fix posts. The first one’s listing presents it as a stopgap, while you find out why the scheduler is not working.
Debugging with WP Crontrol
Reading the task list
WP Crontrol, free, maintained by John Blackbourn, the author of Query Monitor, and installed on more than 300,000 sites as of October 2026, shows every scheduled task with its arguments, interval, callbacks and next due date. It lets you run, pause, edit or delete them, and flags those with no callback attached or that missed their due date.
Deleting a task that keeps coming back is pointless: its plugin recreates it. Pause its name instead. A task with no callback usually comes from a deleted plugin and can be removed. WP Crontrol does not yet keep a run history; for a log, Advanced Cron Manager offers one in its paid version.
Spawn errors
When tasks do not run at all, the cause is often the loopback request: DNS, firewall, HTTP authentication, certificate, missing wp-cron.php file or a fatal PHP error. The wp cron test command checks that spawning works, as long as WP-Cron is not disabled, and WP Crontrol’s help lists these causes one by one.
WP Crontrol PHP and URL tasks
WP Crontrol also lets you create tasks that run PHP code or call an address. That is powerful, and therefore sensitive: in March 2024, a flaw scored 8.1 out of 10 allowed remote code execution provided another flaw already existed on the site or the database had been compromised, fixed in version 1.16.2. Since version 1.18, the CRONTROL_DISALLOW_PHP_EVENTS constant disables PHP tasks. Use it on sites that do not need them.
A security eye
The wp-cron.php file runs on every spawn, which makes it a target. In 2025, Wordfence described a fake security plugin accompanied by a modified wp-cron.php that re-created and reactivated it on the next visit if it was deleted, and one variant of which used a task scheduled every minute to contact its command server. An unknown task on an unusual interval deserves investigation, and wp core verify-checksums checks that core files have not been modified.
Monitoring cron health
Site Health, WP-CLI and Action Scheduler
Site Health reports a late or failed task. Without DISABLE_WP_CRON, a task more than five minutes late is considered failed; with the constant, the thresholds move to fifteen minutes and one hour, to allow for a system cron’s pace. This test only looks at the WP-Cron list, not at the Action Scheduler queue, which shows its own admin warning as soon as a job is more than a day overdue.
An external heartbeat
No internal check can see a system cron that has stopped running altogether. A monitoring service such as Healthchecks.io expects a signal on every run and alerts when it does not arrive. All it takes is adding a call at the end of the cron line, for example && curl -fsS -m 10 --retry 5 -o /dev/null followed by the address the service provides.
Healthchecks.io offers a free plan with twenty checks at the time of writing, and can also be self-hosted. Cronitor, another service of the same kind, offers a free plan with five monitors. Our WordPress maintenance checklist lists the other checks to run regularly.
Summary table
| Method | Trigger | Limits | Best for |
|---|---|---|---|
| Default WP-Cron | Visits that reach PHP | Delays, fragile loopback, cache | Small sites with no timing stakes |
| ALTERNATE_WP_CRON | Visitor redirect | Special case, fragile | Temporary troubleshooting |
| System cron by URL | Server scheduler | Depends on network and authentication | Most sites |
| System cron with WP-CLI | Server scheduler | Lock to add, SSH access | Stores, busy sites |
| Host runner | Host service | Imposed pace | Sites on managed hosting |
Frequently asked questions
Should WP-Cron always be disabled?
No, only if you replace it with a system cron or your host does. A small site with no urgent tasks can keep the default behavior. Disabling without replacing is the worst option.
Which interval should the system cron use?
Five to fifteen minutes suits most sites. A store with subscriptions or many background jobs benefits from one or five minutes, with a lock to avoid simultaneous runs.
Can wp-cron.php be called every minute?
Yes, but each call loads WordPress, even when no task is due. Kinsta sets five minutes as the minimum interval for its custom crons. One minute is justified for a very active store; for most sites, five to fifteen minutes are enough.
WP-CLI or a URL call?
WP-CLI if you have SSH access and the site is protected by authentication or a firewall. The URL call is simpler to set up on shared hosting, and keeps core’s lock.
Why do my scheduled posts still miss?
First check that a trigger really exists: DISABLE_WP_CRON without a system cron stops everything. Then, if WP-Cron is not disabled, test spawning with wp cron test, which errors as soon as DISABLE_WP_CRON is set. With a system cron, check the task list with wp cron event list instead, and look in WP Crontrol for whether the publishing task exists and when it is due.
Does Action Scheduler need WP-Cron?
Not in theory, but WP-Cron is what starts it by default. If you disable WP-Cron, keep a system cron that calls wp-cron.php or run the queue with WP-CLI, otherwise Action Scheduler jobs stop along with everything else.
Did WordPress 6.9 solve the problem?
It removed the visitor’s wait before the page is shown, not the dependency on visits nor the work inside a visitor’s PHP process. A system cron remains the reliable solution.
Conclusion
WP-Cron is a clever compromise: it makes scheduled tasks work on any hosting, with no configuration. That compromise is paid for in delays on quiet sites, load on busy ones, and fragility as soon as the server can no longer call itself.
For a site that matters, the recipe fits in three lines: turn off the visit-triggered spawn, set up a system cron, by URL or WP-CLI, and monitor that it runs with an external signal. On a store, add a line for Action Scheduler.
Sources
- WordPress Developer Resources. Cron
- WordPress Developer Resources (March 2025). Hooking WP-Cron Into the System Task Scheduler
- WordPress Developer Resources. Understanding WP-Cron Scheduling
- WordPress Developer Resources. Scheduling WP Cron Events
- WordPress Developer Resources. wp_cron()
- WordPress Developer Resources. wp_schedule_single_event()
- WordPress Developer Resources (August 2026). Editing wp-config.php
- WP-CLI. wp cron event run
- WP-CLI. wp cron test
- WP-CLI Handbook. Common issues and their fixes
- Make WordPress Core, Weston Ruter (November 18, 2025). WordPress 6.9 Frontend Performance Field Guide
- GitHub, WordPress, Peter Wilson (October 12, 2025). Cron API: Spawn cron jobs on shutdown hook
- GitHub, WordPress, Weston Ruter (November 27, 2025). Restore prior wp_cron() logic when ALTERNATE_WP_CRON is enabled
- GitHub, WordPress/Requests (August 25, 2023). Issue 826, blocking => false not working
- WordPress.org News (December 2, 2025). WordPress 6.9 “Gene”
- Make WordPress Core (August 5, 2026). WordPress 7.1 Field Guide
- WordPress.org Plugins. WP Crontrol
- WP Crontrol. Cron events that have missed their schedule
- WP Crontrol. Problems spawning a call to the WP-Cron system
- WordPress.org Plugins. Action Scheduler
- Action Scheduler. Background Processing at Scale
- Action Scheduler. WP-CLI
- Action Scheduler. FAQ
- GitHub, WooCommerce. Action Scheduler, Disable Default Queue Runner
- WooCommerce. Subscriptions Scheduled Action Errors
- WordPress VIP Documentation. WP-Cron
- WP Engine. Configure wp-cron and Event Scheduling
- Kinsta, Brian Jackson (June 2026). How to Disable WP-Cron for Faster Performance
- Pantheon Docs. Cron for WordPress
- SpinupWP. Understanding WP-Cron
- WordPress.org Plugins. MWW Scheduled Post Trigger
- WordPress.org Plugins. Advanced Cron Manager
- BleepingComputer, Bill Toulas (April 30, 2025). WordPress plugin disguised as a security tool injects backdoor
- Healthchecks.io. Documentation
- WordPress.org Documentation. Site Health Screen
- Make WordPress Core (December 1, 2025). WordPress 6.9 Release Candidate 4
- WooCommerce. Complete guide to scheduled events with Subscriptions
- WooCommerce. WooCommerce Subscriptions Health Check
- WooCommerce Developer Blog (June 17, 2026). What’s changing in Action Scheduler 4.0.0
- GitHub Advisory Database (March 25, 2024). WP Crontrol vulnerable to possible RCE when combined with a pre-condition (CVE-2024-28850)
- Infosecurity Magazine (April 29, 2025). WordPress Malware Masquerades as Anti-Malware Plugin
- Healthchecks.io. Plans and Pricing
- Cronitor. Pricing
- Kinsta Docs. Cron jobs
LaFactory designs, builds and maintains WordPress and WooCommerce sites, and develops its own plugins. Talk to us about your WordPress project.
