An Action Scheduler stuck on thousands of pending jobs is almost never a bug in Action Scheduler. It is a site where nothing reliable is triggering the queue runner, and the pile-up you are looking at under WooCommerce → Status → Scheduled Actions is the symptom rather than the fault. Fix the trigger and the backlog drains on its own.
This matters more than it sounds. Action Scheduler is the background job queue that WooCommerce and dozens of other plugins run on — order emails, subscription renewals, stock syncs, analytics imports, the HPOS backfill. When the queue stalls, none of that happens, and nothing errors loudly enough to page you. Customers just stop receiving emails.
Where to look before you change anything
What a stalled queue actually looks like
The Scheduled Actions screen splits everything by status. Reading those counts correctly saves you from fixing the wrong thing, because a thousand pending actions scheduled for next month is a healthy queue and twelve pending actions from last Tuesday is a dead one.
| Status | What it means | What a large number here is telling you |
|---|---|---|
| Pending | Scheduled, not yet run | Check the dates, not the count. Anything past due by more than a day means the runner is not firing |
| In-progress | Claimed by a runner and currently executing | More than a handful at once, or ones that sit here for minutes, means batches are dying mid-run |
| Complete | Ran without throwing | Only a storage concern. These are purged on a schedule — see the retention section below |
| Failed | Threw an exception, or was still in-progress past the timeout | Open the row and read its log. This is the only status that tells you why |
| Canceled | Unscheduled before it ran | Normal. Recurring actions cancel and reschedule constantly |
The library’s own documentation is blunt about the pending column: some past-due actions are normal because of how WP-Cron works, but several past-due by more than a day means something is wrong with the site. That is the line to judge yourself against.
Why your Action Scheduler is stuck on pending actions
There are six causes worth checking, and in my experience the first two account for most stalled queues on shared and managed hosting.
Diagnose it in four commands
If you have SSH and WP-CLI, you can separate “cron is broken” from “the queue is just big” in under a minute. Run these in the WordPress root.
# 1. Counts per status, plus which datastore is in use
wp action-scheduler status
# 2. The oldest pending actions — look at the scheduled dates
wp action-scheduler action list --status=pending --per-page=20
# 3. Can WordPress spawn its own cron at all? This is the loopback test
wp cron test
# 4. Is the queue runner even scheduled?
wp cron event list | grep action_scheduler
Command three is the one that usually breaks the case open. If wp cron test cannot spawn a request, your loopbacks are blocked and no amount of tuning will help until that is fixed. The same check lives in the admin under Tools → Site Health, which flags both failed loopback requests and a missed cron schedule — WordPress Site Health: how to find and fix common problems covers reading that screen properly.
No SSH? Check Site Health for the loopback and cron warnings, then look at whether DISABLE_WP_CRON appears in your wp-config.php. Those two facts cover most of what the commands above tell you.
Fix the trigger: give WP-Cron a real schedule
This is the permanent fix, and it is worth doing on every production store whether or not you currently have a backlog. Stop depending on visitor traffic to run background work.
First, turn off the traffic-driven spawner in wp-config.php, above the “stop editing” line:
<?php
define( 'DISABLE_WP_CRON', true );
Do not stop after that line
DISABLE_WP_CRON without adding a real cron job is the single most common way a site ends up with a permanently frozen queue. The constant does not move cron to the server — it only stops WordPress from running it. If you cannot add the cron job in the same sitting, leave the constant out.Then add a system cron job. Run crontab -e as the user that owns the site files and add one line, adjusting the paths:
# Run due events every minute, silently
* * * * * cd /var/www/example.com && /usr/local/bin/wp cron event run --due-now --quiet > /dev/null 2>&1
Use which wp to get the real binary path — cron does not inherit your shell’s PATH, and a bare wp is the usual reason a crontab line does nothing. If your host has no shell access, most managed hosts expose a cron panel that can hit https://example.com/wp-cron.php?doing_wp_cron on a schedule instead. That is less efficient than the CLI route but far better than nothing.
Scheduled posts failing to publish is the same underlying problem wearing a different hat; if you have seen that too, the missed schedule error in WordPress walks through the cron side in more detail.
Clear an existing backlog with WP-CLI
Fixing the trigger stops the bleeding. It does not clear what has already piled up, and at one batch a minute a large backlog can take days. Run the queue directly instead — it bypasses the web server entirely, so it does not compete with shoppers for PHP workers.
# Run everything due, 100 per batch, until the queue is empty
wp action-scheduler run
# Bigger batches, and override the concurrency guard
wp action-scheduler run --batch-size=200 --force
# Only one plugin's queue — use the value from the Group column
wp action-scheduler run --group=my-plugin-group --batches=10
# Everything except one noisy group
wp action-scheduler run --exclude-groups=my-plugin-group
Two things to know. The CLI runner defaults to 100 actions per batch, which is deliberately larger than the 25 the web runner claims. And --force exists precisely because the concurrency guard will otherwise refuse to start a second runner while a web batch is in flight — useful when a crashed batch has left a stale claim behind.
On a backlog of any size, run it inside screen or tmux. An SSH disconnect halfway through a six-hour drain is a bad afternoon. Watch progress from a second session with wp action-scheduler status.
Tuning the runner: three filters and their real defaults
If the queue keeps outpacing the runner even with cron working, these are the three knobs. Raise them carefully and one at a time, because each one trades throughput for load on a web server that also has customers on it.
| Filter | Default | What raising it buys you | What it costs |
|---|---|---|---|
action_scheduler_queue_runner_time_limit | 30 seconds | More actions processed per request | Must stay below your PHP max_execution_time and the host’s request timeout. Published host ceilings: WP Engine 60s, Pantheon 120s, SiteGround 120s |
action_scheduler_queue_runner_batch_size | 25 actions | Fewer claim queries per action processed | Only helps when actions finish fast. With slow actions you just claim rows you will not reach before the time limit |
action_scheduler_queue_runner_concurrent_batches | 1 batch | Parallel runners, so throughput scales | The highest-risk of the three. Each batch is another loopback request and another PHP worker. The library’s own docs warn this “can substantially increase server load and take down a site” |
Put the overrides in a small must-use plugin rather than your theme’s functions.php, so a theme switch cannot silently revert them:
<?php
// wp-content/mu-plugins/action-scheduler-tuning.php
add_filter( 'action_scheduler_queue_runner_time_limit', function () {
return 90; // seconds — confirm your max_execution_time allows it
} );
add_filter( 'action_scheduler_queue_runner_batch_size', function () {
return 50; // default is 25
} );
Before you raise concurrency, be honest about whether you have the headroom. Extra batches consume PHP workers that would otherwise be serving checkout requests, and on a plan with four workers, three background batches means one worker left for the store. WooCommerce hosting requirements: PHP workers and RAM covers how to work out what you actually have. If the admin is already sluggish, raising concurrency will make it worse — start with diagnosing the slow admin dashboard instead.
The better answer for sustained high volume is not more loopbacks. It is a dedicated WP-CLI runner on a cron schedule, which is what the library’s own performance guidance recommends.
Actions stuck in-progress, then suddenly failed
This is a specific and frequently misread pattern. An action goes in-progress, sits there, and five minutes later flips to failed with a message along the lines of “unknown error occurred — check server, PHP and database error logs to diagnose cause”.
Nothing mysterious is happening. Action Scheduler’s queue cleaner marks any action that has been running longer than a timeout as failed, on the assumption that an uncatchable fatal error killed the process. That timeout is 300 seconds by default and is filterable:
<?php
// Give genuinely long-running jobs more rope — or pass a negative
// value to disable the check entirely (rarely a good idea).
add_filter( 'action_scheduler_failure_period', function ( $seconds ) {
return 600;
} );
So the five-minute delay is the cleaner noticing, not the moment of failure. The action died earlier, and it died in a way PHP could not log through WordPress. Three usual suspects, in order of likelihood:
- A PHP fatal or memory exhaustion in the callback. The request dies before anything can be written to the action’s log. Your server’s PHP error log has it — enabling WordPress debugging and safely reading error logs covers getting at it without exposing notices to visitors, and the allowed memory size exhausted error covers the memory case specifically.
- The host killed the request. If your actions routinely run longer than the time limit, and the time limit is above the host’s own request timeout, the process is terminated mid-action. Lower the Action Scheduler time limit to sit under the host’s ceiling rather than above it.
- A database deadlock while claiming. Two runners contending on the claims table under load. Reducing concurrency, not increasing it, is the fix here.
When a single hook is responsible, the Scheduled Actions screen will show it — sort by hook and the failures cluster. That gives you the plugin. From there it is an ordinary plugin problem, and troubleshooting a WordPress plugin conflict step by step applies unchanged.
Table bloat and what gets cleaned up automatically
Action Scheduler 3.0 and later stores everything in four custom tables rather than in wp_posts: wp_actionscheduler_actions, wp_actionscheduler_claims, wp_actionscheduler_groups and wp_actionscheduler_logs. The logs table is usually the largest, because every status change on every action writes a row.
There is automatic cleanup, and the real defaults are worth knowing because they are longer than most people assume:
| Filter | Default | Applies to |
|---|---|---|
action_scheduler_retention_period | 2,678,400 seconds — 31 days | Complete and canceled actions |
action_scheduler_retention_period_for_failed | Three times the above — roughly 93 days | Failed actions, kept longer so you can still investigate them |
action_scheduler_cleanup_batch_size | 20 actions per status, per run | How much the cleaner deletes at a time |
ActionScheduler_QueueCleaner in the library’s current source. Automatic cleanup of failed actions arrived in Action Scheduler 4.0.Notice the batch size: 20 per status per run. If you generated two million rows during an import, the automatic cleaner will not meaningfully dent it. Do that job explicitly:
# Delete complete and canceled actions older than seven days,
# 500 at a time, pausing a second between batches to spare MySQL
wp action-scheduler clean --status=complete,canceled --before='7 days ago' --batch-size=500 --pause=1
Back up before you delete rows, and never truncate the tables
clean with an explicit --status so pending and in-progress rows are never touched, and take a database backup first.The --pause flag matters more than it looks on a shared database. Deleting half a million rows without it will spike I/O and can slow the whole store for the duration.
Where the queue is a symptom of something else
Order emails not arriving. If emails are queued through Action Scheduler, a stalled queue looks exactly like a mail delivery problem. Check the queue first, then the mail path — WordPress emails not sending covers the second half once you have ruled out the first.
An HPOS migration that never finishes. The order backfill runs through Action Scheduler at 25 orders per batch, so a frozen queue means a migration that sits at the same count for days. Run it from the CLI instead; WooCommerce HPOS migration: risks and real gains has the full sequence and the verification steps.
A queue that grows no matter what you do. At some point the answer is not tuning. A plugin scheduling tens of thousands of actions an hour on four PHP workers is a capacity problem, and the honest fix is either fewer actions or a bigger box.
A ten-minute recovery sequence
- Take a database backup. Everything below either deletes rows or runs code you have not run before.
- Run
wp action-scheduler statusand note the pending count and the oldest date. - Run
wp cron test. If it fails, fix loopbacks before anything else — usually a security plugin rule or basic auth. - Check
wp-config.phpforDISABLE_WP_CRON. If it istrue, confirm a system cron job actually exists. - Add the per-minute
wp cron event run --due-nowcrontab line. This is the permanent fix. - Drain the backlog with
wp action-scheduler run --batch-size=200 --force, insidescreen. - Filter the screen to Failed, sort by hook, and read the log on the most common one. That is your actual bug.
- Once the queue is current, clean old complete and canceled rows with an explicit
--statusand a--pause.
Step five is the one that stops this recurring. Everything else on the list is cleanup you will be doing again in three months if you skip it.
Frequently asked questions
Is it safe to delete pending scheduled actions?
No, not as a way of clearing a backlog. Pending rows are work that has been promised — subscription renewals, scheduled emails, an unfinished migration. Deleting them does not cancel that work or cause it to be rescheduled; it just means it never happens. Clean complete and canceled rows freely, and leave pending and in-progress alone.
How many past-due actions are normal?
Some are always normal, because WP-Cron only fires on page loads and the runner has a time budget. The library’s own guidance is the useful threshold: several actions past due by more than a day means something is wrong. Judge by the oldest scheduled date, not by the raw count.
Why do my actions sit in-progress for five minutes and then fail?
Because the queue cleaner marks anything running longer than 300 seconds as failed, assuming an uncatchable fatal killed it. The five minutes is the cleaner noticing, not the failure itself. Look in your PHP error log for the real cause, and raise or lower the window with the action_scheduler_failure_period filter if your jobs are legitimately long-running.
Will disabling WP-Cron fix a stuck Action Scheduler queue?
Only if you replace it with a real system cron job in the same sitting. DISABLE_WP_CRON on its own removes the only trigger the queue has, which guarantees a permanently frozen queue rather than an unreliable one. The combination of the constant plus a per-minute wp cron event run --due-now is what you want.
Can I run Action Scheduler without WooCommerce?
Yes. It is a standalone library bundled by many plugins, and on a site without WooCommerce its screen appears under Tools → Scheduled Actions. Whichever plugin loads the newest copy provides it for everyone, so the queue and the tables are shared across every plugin using it.
Should I increase concurrent batches to clear a backlog faster?
It is the last thing to try, not the first. Each extra batch is another loopback request taking a PHP worker that could be serving a customer, and the library’s own documentation warns it can take a site down. Run wp action-scheduler run over SSH instead — it gets you more throughput with none of that risk, because it never touches the web server.
How large should the Action Scheduler tables be?
There is no fixed number, but if wp_actionscheduler_logs is in the millions of rows on a modest store, cleanup has not been keeping up. Complete and canceled actions are kept 31 days by default and failed ones about three times that, so anything much older than those windows points at a cleaner that is not running — which is usually the same stalled-cron problem as the backlog itself.
Sources
- Background Processing at Scale — the queue runner’s time limit, batch size and concurrency defaults, and the host time limits quoted above
- Action Scheduler WP-CLI reference — every command and flag used here, including the CLI runner’s 100-action default batch
- Scheduled Actions administration screen and the project FAQ — screen locations, custom table storage, and the past-due guidance
ActionScheduler_QueueCleanersource — the 31-day retention period, the failed-action retention multiplier, the 20-row cleanup batch and the 300-second failure window- Action Scheduler releases — for the 4.0 change that began purging failed actions automatically

Leave a Reply to This Post