WooCommerce Action Scheduler Stuck Jobs: Real Fix

WooCommerce Action Scheduler jobs stuck in pending or in-progress? Clear the queue, fix the runner, and stop subscriptions and emails from silently breaking.
WooCommerceWordPressAction Scheduler
May 10, 20266 min read1006 words

The Problem

A client pinged me at 7am: subscription renewals had stopped firing for two days, follow-up emails were not sending, and the inventory sync with their warehouse had gone silent. Logs were clean. No PHP errors, no failed payments, no plugin updates overnight. The WooCommerce status page told the whole story in one line:

Action Scheduler: 14,832 pending actions
Oldest pending: 2 days ago

Action Scheduler is the cron-of-cron that powers half of WooCommerce: subscriptions, refunds, recurring emails, REST API webhook delivery, stock sync, abandoned cart recovery. When its queue jams, every async task in the store quietly stops. Customers do not see errors. You do not see errors. The store just stops doing things.

If you are looking at thousands of pending actions and cannot work out why the runner is not picking them up, here is the playbook I follow on every site.

Why It Happens

Three things cause Action Scheduler to wedge.

WP-Cron is the runner and WP-Cron has stalled. Action Scheduler ships with a self-triggering loop, but on most hosts it falls back to WP-Cron to start the queue. If DISABLE_WP_CRON is set to true and the replacement system cron is missing or pointed at the wrong URL, nothing kicks the runner. Pages stop being visited (overnight, low traffic), nothing fires.

A previous batch crashed mid-claim and left rows locked. Every time a runner picks up actions, it inserts a row into wp_actionscheduler_claims and stamps the action's claim_id column. If the PHP process is killed (OOM, request timeout, fatal in a hooked callback), the rows stay marked as in-progress forever. The next runner skips them because they are "already being worked on."

A single broken action poisons the queue. Action Scheduler runs actions in priority + scheduled-date order. If the next action throws a fatal that kills the worker, every subsequent action behind it stalls. The worker dies, the action is re-claimed, dies again, and you accumulate failed actions while pending ones build up behind the wedge.

The Fix

Step 1: Confirm the runner is actually firing. Drop this into wp-cli to see the real Action Scheduler state, not the dashboard summary:

wp action-scheduler runner --batch-size=25 --loglevel=info

If it processes a batch immediately, your queue is fine and the runner is the problem — WP-Cron is not pinging it. If it errors on the first action, you have a poison pill. If it sits there saying No actions to run while the dashboard shows 10,000 pending, you have stuck claims.

Step 2: Release stuck claims. This is the most common fix and the one nobody documents clearly. Run this SQL against your DB (back it up first):

UPDATE wp_actionscheduler_actions
SET claim_id = 0, status = 'pending'
WHERE status = 'in-progress'
  AND last_attempt_gmt < (NOW() - INTERVAL 1 HOUR);

DELETE FROM wp_actionscheduler_claims
WHERE date_created_gmt < (NOW() - INTERVAL 1 HOUR);

This releases any action that has been "in-progress" for more than an hour back to pending and drops the orphaned claim rows. After this, run the runner again — the previously frozen actions should now process.

Step 3: Fix WP-Cron permanently. Stop trusting visitor traffic to fire your queue. Disable WP-Cron and run a real system cron:

// wp-config.php
define('DISABLE_WP_CRON', true);

Then on the server (cPanel, Cloudways, plain Linux):

*/1 * * * * curl -s https://yoursite.com/wp-cron.php?doing_wp_cron > /dev/null 2>&1

Every minute. Action Scheduler hooks into action_scheduler_run_queue which fires from WP-Cron, so a one-minute pulse keeps the queue chewing through. On WP Engine and Kinsta the system cron is built in — turn off the alternate WP-Cron toggle in their dashboards and you are done.

Step 4: Find and disable the poison pill. If the queue freezes again after a few batches, you have a fatal in a callback. Identify it:

wp action-scheduler list \
  --status=failed \
  --hook \
  --fields=hook,args,last_attempt_gmt \
  --format=table \
  --orderby=last_attempt_gmt \
  --order=DESC | head -20

The hook column is the failing action name. Common offenders: woocommerce_subscriptions_scheduled_subscription_payment (failing payment gateway), wcs_create_renewal_order (inventory rules throwing), and action_scheduler/migration_hook (DB upgrade incomplete).

Open wp-admin → Tools → Scheduled Actions, filter by that hook, click into one, copy the args, and grep your codebase or plugin for the add_action() registration. The fix is usually a try/catch around the offending callback so the action marks as failed gracefully without taking the worker down with it.

Step 5: Cap retention so the table does not balloon. Sites that have been running for years end up with millions of wp_actionscheduler_actions rows, which slows queries and makes the queue feel sluggish even when not stuck. Add this to functions.php or a mu-plugin:

add_filter('action_scheduler_retention_period', function () {
    return DAY_IN_SECONDS * 30;
});

Combined with the daily cleanup that Action Scheduler runs by default, this keeps the table under a few hundred thousand rows on busy stores. See the Action Scheduler usage docs for the other filters worth knowing about.

The Lesson

Action Scheduler failures are silent because the system is silent by design — async things that "should" happen later. Verify the runner with WP-CLI before anything else, release stuck claims with one SQL update, and replace WP-Cron with a system cron so traffic patterns can never starve your queue again.

If subscriptions, webhooks, or stock sync have gone quiet on your store and you need someone to dig the queue out without disturbing live orders, this is the kind of work I do — see my services, or read the related WooCommerce Stripe webhook signature mismatch fix for another silent failure mode worth knowing about.

Back to blogStart a project