WP Webhooks / Blog / Architecture
Article · Architecture

WooCommerce Action Scheduler MySQL Deadlocks: The Fix

Why WooCommerce Action Scheduler throws MySQL deadlocks on release_claim with concurrent queue runners — and how to stop them safely.

8 min 2026-06-25
#action-scheduler#woocommerce#wordpress

TL;DR: Action Scheduler deadlocks come from the UPDATE inside claim_actions() colliding when two or more queue runners reserve overlapping rows in wp_actionscheduler_actions at the same instant.

  • Fastest fix: set action_scheduler_queue_runner_concurrent_batches to 1 so one runner claims at a time, and use a smaller batch size to hold the lock for less time.
  • Make sure you are on Action Scheduler 3.2.0 or newer — it added an index that cut claim-query lock contention.
  • A MySQL deadlock is transient, not data loss: InnoDB rolls back one transaction and expects a retry. Do not treat error 1213 as fatal.

/ Root cause

Why does Action Scheduler throw MySQL deadlocks?

Because two or more queue runners try to claim overlapping rows in wp_actionscheduler_actions in the same moment. Each runner issues a single statement like UPDATE wp_actionscheduler_actions SET status = 'in-progress', claim_id = N ... ORDER BY ... LIMIT 25. InnoDB takes row and gap locks in the order rows match the query. When two runners lock the same rows in opposite order, InnoDB detects the cycle and kills one transaction with Deadlock found when trying to get lock; try restarting transaction — MySQL error 1213.

On a busy WooCommerce store the pending table is large and several runners fire every minute, so the window where two claim queries overlap is wide. This is a documented, intermittent symptom rather than a corruption bug — see the long-running "Diagnose intermittent deadlocks" issue on the Action Scheduler tracker.

/ The mechanism

What are claim_actions() and release_claim()?

The claim is Action Scheduler's concurrency primitive. ActionScheduler_DBStore::claim_actions() reserves a batch: it stamps a unique claim_id onto up to batch_size pending rows and flips them to in-progress in one UPDATE. That stamp is what lets several runners work the same queue without ever executing the same action twice.

When the batch finishes — or the runner process dies — release_claim() clears that claim_id so any unprocessed actions return to the pool for another runner. The important detail for debugging: the deadlock lives in the claim UPDATE, not in your action callback. The callback never ran; the runner could not even reserve its work. You can read both methods in the ActionScheduler_DBStore source.

Two Action Scheduler queue runners deadlocking on the same claim windowBoth queue runners issue an UPDATE on the pending wp_actionscheduler_actions rows limited to 25 rows and lock the same rows. InnoDB detects the lock cycle and rolls back one transaction with error 1213 while the other claim succeeds.

locks rows #1..#25

locks rows #1..#25

wp_actionscheduler_actions
pending rows #1..#N

Runner A
UPDATE ... LIMIT 25

Runner B
UPDATE ... LIMIT 25

overlap on the same rows

InnoDB detects lock cycle

ERROR 1213
one txn rolled back

other claim succeeds

FIG 01 — Two runners colliding on the same claim window

/ Multipliers

What makes concurrent runners collide?

Three things widen the collision window. First, action_scheduler_queue_runner_concurrent_batches greater than 1 dispatches several loopback runners per cron tick — each one issues its own claim UPDATE. Second, double-driven cron: a traffic-triggered WP-Cron and a system cron can both spawn runners at the same time. Third, a slow claim query holds its locks longer.

That last one is the quiet killer. When the pending table is large or missing the right index, the claim UPDATE scans more rows and keeps gap locks open longer, so overlapping runners are far more likely to deadlock or hit lock-wait timeouts — the exact behaviour tracked in issue #1104, "Slow claim actions update query causing lock wait timeouts". More pending rows plus a longer UPDATE equals more overlap.

A MySQL deadlock is not data loss. InnoDB rolls back one transaction and expects you to retry it. The bug is treating error 1213 as fatal. — InnoDB locking model

/ The fix

How do I stop the deadlocks safely?

Work the cheapest, highest-leverage fixes first. Most stores stop seeing 1213 after the first two.

  1. Upgrade Action Scheduler to 3.2.0 or newer. That release added an index to the actions table specifically to reduce claim-query contention — see the Action Scheduler 3.2.0 release notes. WooCommerce bundles its own copy, so update WooCommerce too.
  2. Drop concurrent batches to 1. If you do not need the parallel throughput, serialize the claim so only one runner reserves rows at a time. This removes the collision entirely.
  3. Use one cron source. Disable visitor-triggered WP-Cron and drive the queue from a single system cron, so you never have two schedulers spawning runners on top of each other.
  4. Treat a stray 1213 as transient. An occasional deadlock under heavy load is expected. Action Scheduler re-claims rolled-back rows on the next tick; alert on sustained spikes, not single events.

mu-plugins/as-deadlock-guard.php — serialize the queue runner

// wp-content/mu-plugins/as-deadlock-guard.php
// One runner claims at a time — removes the claim-overlap deadlock.
add_filter( 'action_scheduler_queue_runner_concurrent_batches', function () {
    return 1;
} );

// Smaller batches hold the claim lock for less time.
add_filter( 'action_scheduler_queue_runner_batch_size', function () {
    return 10;
} );

/ Diagnose first

How do I confirm it is a claim deadlock and not something else?

Check the MySQL error log (or your host's slow/error query view) for Deadlock found when trying to get lock on an UPDATE against wp_actionscheduler_actions that sets status and claim_id. If the failing statement targets that table and column pair, it is the claim. A deadlock inside one of your own action callbacks is a different problem — that points at the work the action does, not at Action Scheduler.

For live queue pressure, open WooCommerce → Status → Scheduled Actions or run wp action-scheduler queue-status as documented in the WP-CLI reference. A pending count in the thousands that keeps growing tells you the claim is contended and the table is big enough for the slow-query path to bite.

ConcernHand-tuned Action SchedulerWebhook Actions
ConcurrencyMultiple loopback runners claim the same rows, triggering 1213 deadlocksSingle persistent delivery queue with a serialized claim
On deadlockAction left in-progress until the claim times outAutomatic retry with exponential backoff, fully logged
Visibilitygrep the MySQL error log to find the failing UPDATEPer-attempt delivery log in wp-admin with response codes

/ The deeper fix

Should you move webhook delivery out of the shared table?

If the deadlocks started after you began enqueuing an outbound HTTP call per order or per form entry, the contention is self-inflicted. You are mixing your delivery workload into WooCommerce's own scheduled-action table, so your webhook claims and WooCommerce's order, email, and sync claims all fight for the same rows.

A purpose-built delivery queue with its own tables and a single serialized runner removes that cross-contention — your deliveries never lock against WooCommerce's housekeeping. That is exactly the boundary the Webhook Actions plugin draws. For the wider failure modes of running delivery on shared cron infrastructure, see why WordPress webhooks silently fail in production.

Webhook Actions keeps outbound delivery in its own queue with smart retry and a full attempt log — so a busy WooCommerce store's scheduled actions and your webhook deliveries never deadlock against each other.

/Footnotes
¹ Action Scheduler intermittent deadlock discussion: github.com/woocommerce/action-scheduler/issues/530.
² Slow claim query / lock-wait timeouts: github.com/woocommerce/action-scheduler/issues/1104.
³ Index added to reduce deadlocks in Action Scheduler 3.2.0.
FAQ

Common questions always ask.

Don't see yours? Open an issue on GitHub or check the full reference in the API docs.

What causes the "Deadlock found when trying to get lock" error in WooCommerce? +
It is almost always Action Scheduler. Two or more queue runners issue an UPDATE on wp_actionscheduler_actions to claim pending rows at the same instant, and InnoDB kills one transaction with error 1213 to break the lock cycle. The failing statement sets status to in-progress and stamps a claim_id.
Is an Action Scheduler deadlock dangerous or does it lose data? +
No. A MySQL deadlock rolls back one transaction cleanly; the rows are untouched and Action Scheduler re-claims them on the next tick. An occasional deadlock under heavy load is expected. Only sustained spikes indicate a real problem worth tuning for.
How do I reduce Action Scheduler deadlocks? +
Upgrade Action Scheduler to 3.2.0 or newer for the added table index, set action_scheduler_queue_runner_concurrent_batches to 1 to serialize claims, lower the batch size so the claim lock is held briefly, and drive the queue from a single cron source instead of both WP-Cron and a system cron.
What is the claim_id column in wp_actionscheduler_actions? +
claim_id is how a queue runner reserves a batch of actions. claim_actions() stamps a unique claim_id on up to batch_size pending rows and flips them to in-progress so other runners skip them. release_claim() clears it again when the batch finishes or the runner dies.
Does upgrading Action Scheduler fix deadlocks? +
It helps significantly. Action Scheduler 3.2.0 added an index to the actions table specifically to reduce claim-query lock contention. Combined with serializing the runner to one concurrent batch, most stores stop seeing error 1213 entirely.
Ready

Stop losing webhooks.
Start logging them.

$ wp plugin install flowsystems-webhook-actions --activate