---
title: Preventing Race Conditions & Deadlocks in WooCommerce Inventory Management
description: Eliminate overselling and deadlocks during flash sales. In-depth guide to MySQL SELECT FOR UPDATE, Redis distributed locking, and atomic stock decrements.
url: https://wpstack.online/2026/09/19/preventing-race-conditions-deadlocks-inventory
date_modified: 2026-09-12
author: Aditya Bhimrajka
language: en_US
---

Inventory failures rarely appear during a calm manual test. They appear when two checkouts, a refund, a warehouse sync, and an administrator update the same SKU within milliseconds. Each request can look correct in isolation while the combined result oversells a product, restores stock twice, or leaves a queue retrying a deadlocked transaction.

WooCommerce already provides concurrency-aware stock functions. The safest design starts by using those functions consistently, then makes every surrounding workflow idempotent, observable, and small.

## Recognize the read-modify-write race

The classic stock race is deceptively simple:

1. Request A reads a quantity of 5.
2. Request B also reads 5 before A writes.
3. Both subtract 1.
4. Both save 4, although two units were sold.

The bug is the gap between the read and the write. A mutex in PHP memory does not protect separate web workers, queue runners, or servers. A transient is also a poor lock: expiry, cache eviction, and non-atomic cache backends can all break the guarantee.

For normal WooCommerce inventory changes, use `wc_update_product_stock()` with the `increase` or `decrease` operation. WooCommerce deliberately performs the change as a direct database query so it can update the amount in one operation.

```
$product = wc_get_product($product_id);

if (!$product || !$product->managing_stock()) {
    return new WP_Error('stock_not_managed');
}

$new_quantity = wc_update_product_stock(
    $product,
    $quantity_to_reserve,
    'decrease'
);

if (is_wp_error($new_quantity)) {
    return $new_quantity;
}
```

Do not replace that call with “get quantity, subtract, set quantity, save.” Use the product CRUD layer for ordinary product changes and the stock helper for relative stock movements. Directly editing `postmeta` bypasses synchronization, cache invalidation, hooks, and data-store compatibility.

## Let WooCommerce own order stock reduction

WooCommerce core tracks whether line-item stock has already been reduced. Its order-level functions avoid reducing the same item repeatedly and can restore the recorded amount when appropriate. A gateway or fulfillment extension should not add an independent stock decrement merely because it receives a payment callback.

Instead, transition the order through supported APIs and allow the normal WooCommerce lifecycle to decide when stock changes. If custom logic genuinely owns the inventory movement, record that ownership once and make duplicate delivery harmless.

## Make every external event idempotent

Payment providers, warehouse systems, and queue workers retry. A webhook can arrive twice or arrive after an earlier request times out. The handler must distinguish “not processed” from “processed successfully but the response was lost.”

Store a provider event ID in a table with a unique index, or use a unique business key such as provider plus event ID. Claim that key before applying the stock mutation. A duplicate insert then becomes a safe no-op rather than a second decrement.

```
CREATE TABLE wp_inventory_events (
  event_id varchar(191) NOT NULL,
  product_id bigint unsigned NOT NULL,
  created_at datetime NOT NULL,
  PRIMARY KEY (event_id),
  KEY product_id (product_id)
) ENGINE=InnoDB;
```

Do not use an order note as the only deduplication mechanism. Notes are useful evidence, but “check for note, then add note” is another read-modify-write race. A database uniqueness constraint makes the claim atomic.

## Understand deadlocks instead of hiding them

A deadlock is not the same as an oversell. It occurs when concurrent transactions each hold a lock the other needs. InnoDB detects the cycle and rolls one transaction back. On a busy system, occasional deadlocks are expected; repeated deadlocks point to long transactions, missing indexes, or inconsistent access order.

If one operation touches several products, sort the product IDs before updating them. Every worker should acquire records in the same order:

```
$product_ids = array_map('absint', $product_ids);
$product_ids = array_values(array_unique(array_filter($product_ids)));
sort($product_ids, SORT_NUMERIC);

foreach ($product_ids as $product_id) {
    // Apply the validated stock operation in this stable order.
}
```

Keep the locked section short. Do not call a remote API, send email, generate a document, or wait on another queue while a database transaction is open. Validate inputs and prepare data before the first locked write; commit before doing side effects.

When InnoDB reports a deadlock, retry the entire transaction, not only the final SQL statement. Use a small retry limit with jittered backoff, and make sure the transaction is idempotent before retrying it. A lock wait timeout may have different rollback scope depending on database configuration, so treat it deliberately rather than assuming the full transaction disappeared.

## Use custom transactions only for custom invariants

WooCommerce and WordPress do not provide a general transaction wrapper around an entire checkout. Adding `START TRANSACTION` around arbitrary hooks can be dangerous because another plugin may commit implicitly, use a non-transactional table, or perform slow network work.

A custom transaction is appropriate when you own the tables and can define a narrow invariant, such as claiming a unique warehouse event and adjusting a corresponding reservation record. Use InnoDB, index every lookup used by a locking read, lock rows in a consistent order, and guarantee rollback on every error path.

`SELECT ... FOR UPDATE` is useful when a decision depends on a current row value and the subsequent write must exclude competing writers. It only protects rows selected inside a transaction, and a poorly indexed predicate can lock or scan far more than intended. Prefer a single conditional update when one statement can express the invariant.

## Separate reservations from final stock

High-traffic stores sometimes need a reservation model rather than immediate permanent deduction. A reservation has an identity, product, quantity, owner, state, and expiry. It can be confirmed after payment or released after timeout. This is a business workflow, not just a database lock.

Do not add a reservation system unless checkout behavior requires it. WooCommerce already includes stock-hold behavior for unpaid orders. First confirm that the built-in lifecycle and your payment gateway configuration do not already cover the requirement.

## Keep asynchronous work bounded

Action Scheduler is appropriate for warehouse synchronization, reconciliation, and other background work, but a queue does not automatically remove races. Two actions can still touch the same SKU. Give actions stable deduplication keys, use small batches, and do not enqueue one action per item when a bounded batch is cheaper.

Track pending age, failure rate, retry count, and processing duration. A growing queue can leave the storefront displaying quantities that are technically consistent but operationally stale.

## Diagnose with evidence

Start with the most recent InnoDB deadlock report and identify the statements, indexes, and lock order involved. During a controlled diagnostic window, database administrators can enable logging of all deadlocks. Disable verbose logging when the investigation is finished.

For each inventory mutation, log a correlation ID, event ID, order ID, product ID, operation, requested quantity, resulting quantity, and outcome. Do not log card data, credentials, or complete webhook bodies. Correlate these records with Action Scheduler logs and gateway delivery IDs.

## Run a real concurrency test

A useful test fires many simultaneous requests at a low-stock product and asserts the invariant after every worker finishes. Test at least these cases:

- Two successful checkouts compete for the final unit.
- The same payment event is delivered twice.
- A payment confirmation races with cancellation.
- A refund races with a warehouse quantity sync.
- A queue worker is terminated after the database write but before acknowledgement.
- A variable product updates stock on the managing parent or variation as configured.

Assert the database quantity, stock status, order item reduction metadata, event ledger, and customer-visible result. A test that only checks HTTP status codes will miss the failure that matters.

## The reliable default

Use WooCommerce’s atomic relative stock operation, let the core order lifecycle own normal reductions and restorations, deduplicate external events with a database constraint, and keep multi-record work ordered and short. Retry a deadlocked transaction only when the operation is safe to repeat.

For implementation details, consult the official [WooCommerce stock function reference](https://woocommerce.github.io/code-reference/files/woocommerce-includes-wc-stock-functions.html), the [MySQL guidance for handling InnoDB deadlocks](https://dev.mysql.com/doc/refman/8.4/en/innodb-deadlocks-handling.html), and the [MySQL locking reads reference](https://dev.mysql.com/doc/refman/8.4/en/innodb-locking-reads.html).
