WooCommerce

Action Scheduler Stuck: The Queue Behind WooCommerce Emails

Emails delayed, renewals stalled, thousands of actions pending. How Action Scheduler works, why it stalls, how to read its screen, and how to make it reliable.

5 min read
WooCommerceDebuggingHosting
985 words5 min read

What Action Scheduler is

WooCommerce does not send emails, fire webhooks, renew subscriptions or process many of its background jobs at the moment they are triggered. It queues them. The queue is Action Scheduler, a library bundled in WooCommerce and used by Subscriptions, many extensions, and WooCommerce's own analytics and webhooks. It stores actions in its own tables (wp_actionscheduler_actions and friends) and runs them in batches when a runner fires.

When it is healthy, actions run within a minute or two of being scheduled. When it is not, the store's background life stops: order emails arrive hours late, subscription renewals do not happen, analytics stops updating, webhooks to external systems never fire. The symptoms look unrelated. They are all the queue.

Reading the Scheduled Actions screen

WooCommerce, Status, Scheduled Actions. Five tabs:

  • Pending. Actions waiting to run. A healthy store has a modest number, with scheduled dates in the near future. A store with thousands in the past is stuck.
  • In-progress. Actions currently running. More than a handful for more than a minute suggests a runner has died mid-batch.
  • Complete. Done. Retained for a month by default.
  • Failed. Actions that errored or timed out. Read the log for each; it names the hook and the error.
  • Past-due. Pending actions whose scheduled time has passed. The number here is the health metric. It should be near zero.

Each action shows its hook name, which tells you which plugin queued it. Sort Pending by scheduled date and look at the oldest.

Why it stalls

The runner depends on WP-Cron. By default Action Scheduler is triggered by WP-Cron, which is triggered by page loads. On a cached site, or a quiet one, cron does not fire, and neither does the queue. This is the most common cause and it is the same fix as broken WP-Cron: a server cron every minute.

The batch is too small for the volume. A runner processes a limited batch (default 25 actions) within a time limit (default 30 seconds) per run. A store queueing more actions per minute than that clears falls behind forever. Subscriptions renewals at month start, bulk imports, and analytics regeneration all produce spikes.

Long-running actions. One action that takes 40 seconds blows the time limit and can leave the batch marked in-progress. Repeated, the queue grinds. Typical culprits: an extension making slow external API calls in an action, or an action processing thousands of records without chunking.

Failed actions retrying. Some actions reschedule on failure. A permanently failing action (a webhook to a dead endpoint, a renewal for a deleted product) retries indefinitely, consuming runner time each cycle.

Database bloat. Millions of completed actions and their logs, never pruned, make every queue query slow, and the runner spends its 30 seconds querying rather than running. Check the row count of wp_actionscheduler_actions and wp_actionscheduler_logs.

Concurrency lock held. A runner that died hard (a PHP fatal, a server restart) can leave a claim on a batch. Claims expire, but a store with repeated crashes accumulates them.

Memory or execution limits on the host lower than the runner's needs. The runner dies silently mid-batch.

Making it run reliably

1. Run it from server cron, not page loads.

Disable page-load cron and trigger it every minute via WP-CLI:

Text
* * * * * cd /path/to/site && wp action-scheduler run --batch-size=50 --batches=0 > /dev/null 2>&1

wp action-scheduler run processes the queue directly, without the WP-Cron layer, with no HTTP timeouts and with the CLI's memory and time limits rather than the web server's. --batches=0 runs until the queue is clear. This alone fixes most stuck stores.

Alternatively, the "Async Request Runner" (on by default in recent versions) fires a loopback request after page loads to process additional batches; it helps but still depends on traffic and on the site being able to reach itself.

2. Tune the batch for the volume.

Filters action_scheduler_queue_runner_batch_size and action_scheduler_queue_runner_time_limit raise the defaults for stores that need it. With a CLI runner, batch size 100 and no time limit is reasonable; with the web runner, keep the time limit below the server's execution limit.

3. Prune the tables.

action_scheduler_retention_period defaults to 31 days. Stores with high volume can reduce it to 7. If the tables are already huge, delete old completed actions and their logs in batches via SQL or WP-CLI, then let retention hold it. Ensure the tables have their indexes; some migrations have lost them.

4. Find and fix the perma-failures.

Failed tab, group by hook. An action failing repeatedly needs its cause fixed (the dead endpoint, the missing product) or the action cancelled. wp action-scheduler has commands to cancel by hook or group.

5. Fix the long-running actions.

Failed actions with timeout errors name the hook. The plugin responsible needs to chunk its work or move slow external calls out of the action. Report it; sometimes the fix is an update.

6. Monitor Past-due.

A weekly glance at the count, or a small script alerting when it exceeds a threshold. The queue fails silently; the monitor is what makes it loud.

Recovering a badly stuck store

Thousands past-due, emails days late:

  1. Set up the CLI cron runner above.
  2. Run wp action-scheduler run --batches=0 manually and watch it clear. It may take a while.
  3. Prune old completed actions.
  4. Cancel perma-failures.
  5. Watch Past-due return to near zero over the next hour.
  6. Then find out why it stalled, from the list above, so it does not recur.

Where this fits

Action Scheduler health is one of the checks on every store we take on for WooCommerce work and one of the monthly items for stores on a care plan, because so many "the emails are slow" and "renewals stopped" tickets are this one queue, and the fix is a cron line that should have been there from day one.

All writingHire me for this