Clearing millions of pending WooCommerce scheduled actions
A huge Action Scheduler backlog means a stalled runner, a failing hook, or a flooding plugin. How to diagnose it, drain it safely, and stop it recurring.
- WordPress
- WooCommerce
- Operations

It usually shows up indirectly. Subscription renewals stop firing, a follow-up email plugin goes quiet, stock syncs fall behind. Then someone opens WooCommerce → Status → Scheduled Actions and sees millions of pending actions. The screen may take a minute to load, or it may time out. It is tempting to read that number as traffic, as if the store is busy and the queue is just behind. That is almost never the case. A queue that big means one of two things: whatever consumes the queue has stopped, or something is adding jobs much faster than any runner could process them.
A million pending actions is a symptom, not a workload
Action Scheduler is the job queue that WooCommerce and many other plugins share. It stores jobs in wp_actionscheduler_actions. By default it processes them in small batches, triggered by WP-Cron and by an async loopback request. Most runaway queues come from one of three failures:
- The runner is not running. WP-Cron needs page traffic, and it needs the site to be able to make HTTP requests to itself. Any of these stops processing without an error anyone sees:
DISABLE_WP_CRONset with no system cron to replace it, a loopback request blocked by basic auth or a firewall, or a bot-protection layer rejecting the server's own requests. - A hook fails and keeps coming back. An action that throws an error is marked failed. But some plugins reschedule on failure, and a recurring action whose callback errors every time will keep creating new work that never completes.
- A plugin floods the queue. A sync, import or webhook integration that schedules one action per product, order or request can add hundreds of thousands of jobs a day. Many of them are often duplicates.
Each one needs a different fix, so first find out which one you have.
Count by hook before touching anything
The admin screen is the wrong tool for this. Query the database directly and only read:
SELECT hook, status, COUNT(*) AS n
FROM wp_actionscheduler_actions
GROUP BY hook, status
ORDER BY n DESC
LIMIT 30;
Change the table prefix to match your install. Almost always, one or two hooks account for most of the pending rows, and the hook name tells you which plugin is responsible. Next, check how old the pending actions are:
SELECT hook, COUNT(*) AS n,
MIN(scheduled_date_gmt) AS oldest,
MAX(scheduled_date_gmt) AS newest
FROM wp_actionscheduler_actions
WHERE status = 'pending'
GROUP BY hook
ORDER BY n DESC
LIMIT 10;
Read the results like this:
- The oldest pending action is weeks old and nothing new is completing: the runner has stalled.
- The same hook also has a large failed count: a failure is retrying.
- Thousands of pending rows share a hook and identical
args: a plugin is flooding the queue.
For the error text, read the args of a few rows and their matching entries in wp_actionscheduler_logs.
Triage the backlog instead of processing it blindly
Wanting to keep everything is reasonable. Pending actions can include order emails and payment callbacks. But running millions of stale jobs as they are is not automatically safe either. An abandoned-cart reminder that is three weeks old, or a duplicated stock sync, will do the wrong thing if it runs today.
Sort the top hooks into three groups:
- Must run. Order emails, subscription renewals, payment and webhook deliveries. Process these first. Check the owning plugin's documentation to see whether running them late is safe.
- Fine to run late, or to skip. Cache warmers, analytics pings, and repeated syncs where only the latest state matters.
- Should never have been queued. Duplicates created by a flooding plugin.
For group 3, cancel the actions rather than deleting them, so the rows stay as an audit trail. Take a database backup first, then cancel per hook:
wp eval 'as_unschedule_all_actions( "the_offending_hook" );'
Do not truncate the table. Fix the source before you start draining the queue. If you drain first, a plugin that is still flooding will refill it while you work. That can mean updating the plugin, disabling a misconfigured integration, or fixing the callback that throws.
Drain the queue with WP-CLI in bounded runs
wp action-scheduler run handles large queues as long as you limit each run. A single long-running PHP process builds up memory across batches. It can die partway through a batch and leave actions claimed but not finished. Run one hook at a time, in fixed-size chunks, and start a new PHP process for each chunk:
for i in $(seq 1 200); do
wp action-scheduler run --hooks=the_hook_to_process --batch-size=100 --batches=10
sleep 2
done
Run it inside tmux or screen so a dropped SSH session doesn't kill it. Re-run the count query between loops to track progress. Because the store is live, watch CPU and the slow query log while it runs. Work during off-peak hours, and stop the loop if storefront response times rise. Leave out --force. It skips the concurrency check, and the web-triggered runner may still be claiming actions at the same time.
Once the backlog is processed, wp_actionscheduler_actions and wp_actionscheduler_logs will hold millions of completed and canceled rows. Action Scheduler deletes those after its retention period, which defaults to 30 days. To clean up sooner, lower it for a while with the action_scheduler_retention_period filter. When the cleanup has caught up, run OPTIMIZE TABLE on both tables during off-peak hours to reclaim the space.
Move processing to a real system cron
WP-Cron runs only when the site gets page requests, which makes it unreliable for a store that depends on queued work. Disable it in wp-config.php:
define( 'DISABLE_WP_CRON', true );
Then add server cron entries. Most managed hosts, Cloudways included, have a panel for this:
* * * * * cd /path/to/site && wp cron event run --due-now --quiet
*/2 * * * * cd /path/to/site && wp action-scheduler run --batch-size=100 --batches=5 --quiet
The second line gives the queue its own runner, so queue processing no longer depends on WP-Cron or on loopback requests. If throughput still falls short, raise these filters a little at a time rather than all at once: action_scheduler_queue_runner_batch_size, action_scheduler_queue_runner_concurrent_batches and action_scheduler_queue_runner_time_limit. Each concurrent batch is another PHP process competing with checkout.
Put limits and monitoring on the queue
This kind of backlog grows unnoticed because nothing checks the queue. Add a few checks:
- Deduplicate in custom code. Check
as_has_scheduled_action()before scheduling, or pass theuniqueargument toas_enqueue_async_action()andas_schedule_single_action(). This stops one job being queued per page view. - Alert on the age of the oldest pending action, not just the count. A large count can be legitimate for a short time. A pending action that is six hours past its scheduled time means the runner has stopped. A small cron script that runs the age query and posts to your alerting channel is enough.
- Review failed counts per hook weekly. A hook that fails steadily is a bug report waiting to be filed with the plugin vendor.
- Treat a sudden jump in queue volume as a deploy regression. If a plugin update doubles the number of actions scheduled per order, you want to know that the same day.
What to do next
Run the count-by-hook query today, even if nothing looks wrong. It takes seconds and shows you which plugins are using the queue and whether anything is already piling up. If DISABLE_WP_CRON is not set and there is no server cron, set both up this week. That change alone prevents most stalled queues.
We treat Action Scheduler as production infrastructure: diagnosis, triage and draining, the cron setup, and code-level fixes to plugins that flood the queue. That is part of our WordPress and WooCommerce work. If your queue is already in the millions and you don't want to drain it on a live store by trial and error, get in touch.
Need this done on a real stack?
Magento 2, Adobe Commerce, migrations to Medusa.js or Vendure, enterprise Next.js, WordPress, and AI automation.
Contact us