---
title: "ActionScheduler_DBStore Deadlock: The Fix & Resolution"
description: "ActionScheduler_DBStore::claim_actions throws MySQL deadlocks under concurrent WooCommerce queue runners — why it happens and the exact fix."
url: "https://wpwebhooks.org/blog/woocommerce-action-scheduler-mysql-deadlocks/"
date: "2026-06-25"
---

# ActionScheduler_DBStore Deadlock: The Fix & Resolution

**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](https://github.com/woocommerce/action-scheduler/issues/530) on the Action Scheduler tracker. The claim mechanism itself — the `claim_id` column and the dedicated claims table those runners write to — is detailed in the [Action Scheduler database schema](https://wpwebhooks.org/blog/action-scheduler-database-tables/).

/ 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](https://github.com/woocommerce/action-scheduler/blob/trunk/classes/data-stores/ActionScheduler_DBStore.php).

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"](https://github.com/woocommerce/action-scheduler/issues/1104). 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](https://developer.woocommerce.com/2021/06/03/action-scheduler-3-2-0-released/). 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](https://actionscheduler.org/wp-cli/). 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.

| Concern | Hand-tuned Action Scheduler | Webhook Actions |
| --- | --- | --- |
| Concurrency | Multiple loopback runners claim the same rows, triggering 1213 deadlocks | Single persistent delivery queue with a serialized claim |
| On deadlock | Action left in-progress until the claim times out | Automatic retry with exponential backoff, fully logged |
| Visibility | grep the MySQL error log to find the failing UPDATE | Per-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](https://wpwebhooks.org/wordpress-webhook-plugin/) draws. For the wider failure modes of running delivery on shared cron infrastructure, see [why WordPress webhooks silently fail in production](https://wpwebhooks.org/blog/why-wordpress-webhooks-silently-fail-in-production/).

→

[Webhook Actions](https://wpwebhooks.org/wordpress-webhook-plugin/) 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](https://github.com/woocommerce/action-scheduler/issues/530).

² Slow claim query / lock-wait timeouts: [github.com/woocommerce/action-scheduler/issues/1104](https://github.com/woocommerce/action-scheduler/issues/1104).

³ Index added to reduce deadlocks in [Action Scheduler 3.2.0](https://developer.woocommerce.com/2021/06/03/action-scheduler-3-2-0-released/).

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"ActionScheduler_DBStore Deadlock: The Fix & Resolution","description":"ActionScheduler_DBStore::claim_actions throws MySQL deadlocks under concurrent WooCommerce queue runners — why it happens and the exact fix.","datePublished":"2026-06-25","dateModified":"2026-06-25","author":{"@type":"Person","name":"Mateusz Skorupa","url":"https://wpwebhooks.org/about/"},"publisher":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org"},"url":"https://wpwebhooks.org/blog/woocommerce-action-scheduler-mysql-deadlocks/","image":"https://wpwebhooks.org/og_image.jpg","keywords":["woocommerce action scheduler mysql deadlock","action scheduler release claim deadlock","action scheduler concurrent queue runners","actionscheduler actions deadlock","action scheduler claim actions lock","woocommerce scheduled actions deadlock"]}

{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"WP Webhooks","item":"https://wpwebhooks.org/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://wpwebhooks.org/blog/"},{"@type":"ListItem","position":3,"name":"ActionScheduler_DBStore Deadlock: The Fix & Resolution","item":"https://wpwebhooks.org/blog/woocommerce-action-scheduler-mysql-deadlocks/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"What causes the \"Deadlock found when trying to get lock\" error in WooCommerce?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"Is an Action Scheduler deadlock dangerous or does it lose data?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"How do I reduce Action Scheduler deadlocks?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"What is the claim_id column in wp_actionscheduler_actions?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"Does upgrading Action Scheduler fix deadlocks?","acceptedAnswer":{"@type":"Answer","text":"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."}}]}
```
