When stock is supposed to change
WooCommerce reduces stock at a specific moment, and knowing the moment explains most of the failures.
By default, stock is reduced when an order's status moves to processing, completed or on-hold. Not when the order is created, not when the customer clicks Place Order. A pending order has not reduced stock; the items are instead held for a period (default 60 minutes, set in Products, Inventory, "Hold stock") so that other customers cannot buy them while payment completes. If payment never arrives, the hold expires and the pending order is cancelled.
So: order created as pending → stock held → payment confirmed → status to processing → stock reduced. Each arrow is a place to break.
Cause 1: orders never reach a reducing status
Payment succeeds at the gateway, but the order stays pending because the webhook or return did not arrive. No status change, no stock reduction, and after an hour the held stock is released, so the item is sold to a second customer.
Check: orders that are paid at the gateway but pending in WooCommerce. This is a webhook problem, not a stock problem; see the gateway's log and the webhook delivery in the gateway dashboard.
Cause 2: the gateway sets a status that does not reduce
Some gateways move orders to a custom status, or leave them on-hold with stock reduction disabled by a setting, or, for manual payment methods like bank transfer, use on-hold, which does reduce stock by default but which some stores have changed. Check what status each gateway's successful orders land in and whether that status is in the reducing list.
Fix: woocommerce_payment_complete_reduce_order_stock and the status list are filterable; find what a plugin has done to them. Or configure the gateway to set processing on success.
Cause 3: stock management is off
At the product level or globally. Products, Inventory, "Manage stock" must be on; and each product (or each variation, since variations have their own toggle) must have "Manage stock at product level" enabled with a quantity. A variable product with stock managed at the parent but variations set to manage their own, with blank quantities, behaves unpredictably.
Check: the failing product's Inventory tab, and every variation's.
Cause 4: a plugin or snippet has hooked the reduction
woocommerce_can_reduce_order_stock, woocommerce_payment_complete_reduce_order_stock, woocommerce_order_item_quantity, and the status transition hooks are all places a plugin can stop, alter or duplicate stock changes. Inventory sync plugins (to a POS, an ERP, a marketplace) frequently take over stock management and set WooCommerce not to reduce, intending to manage it themselves; when the sync breaks, nothing reduces.
Check: search active plugins and the theme for those hook names. Review any inventory or POS integration's settings.
Cause 5: stock reduced twice
The opposite fault: an order reduces stock on payment complete and again on the status change to processing, or a plugin manually calls wc_reduce_stock_levels in addition to the core behaviour. Orders record when stock was reduced in _order_stock_reduced meta to prevent this; plugins that bypass it cause double reduction.
Check: order notes. Core writes "Stock levels reduced: Product (10→9)" once. Two such notes, or a note plus a plugin's own, means duplication.
Cause 6: refunds and cancellations not restoring
Related, and often reported together. Cancelling an order that reduced stock should restore it; refunding line items offers a "Restock refunded items" checkbox that must be ticked. Deleting an order does not restore stock at all.
Fix: cancel rather than delete; tick restock when refunding; or use woocommerce_order_status_cancelled hooks carefully if a plugin has interfered.
Cause 7: HPOS and sync issues
Stores that migrated to High-Performance Order Storage with compatibility mode on, and a plugin that writes stock changes to post meta rather than through the CRUD API, can end up with orders whose stock-reduced flag is out of sync. Rare, and improving, but worth checking on stores that migrated recently.
Diagnosing a specific case
For an order where stock did not reduce:
- Order status. Pending? Cause 1. On-hold or a custom status? Cause 2.
- Order notes. Is there a "Stock levels reduced" note? If not, reduction never ran. If yes, the product's stock was changed elsewhere afterwards.
- Product inventory settings. Managed, with a quantity, at the right level? Cause 3.
- Gateway log around the order time. Did payment complete fire?
- Plugins. Deactivate inventory-related plugins on staging and replay a test order.
Reconciling
Once the cause is fixed, the stock numbers are wrong for every affected order. Options: a stock take and manual correction on affected products; or a script that walks recent orders in reducing statuses lacking the _order_stock_reduced flag and calls wc_maybe_reduce_stock_levels for each, which applies the reduction idempotently. Take a backup first and run it on staging against a copy to see what it would change.
Preventing it
- Every gateway's successful orders land in processing; tested after each gateway update.
- The hold-stock timer is set to slightly longer than your slowest gateway's confirmation time.
- Inventory plugins own stock or WooCommerce does, never both.
- A monthly spot check: five recent orders, stock before and after in the product history.
That last item is on the care plan checklist for every store we look after, and the hook search in cause 4 is usually where a WooCommerce diagnosis finds the snippet someone added in 2022 and forgot.