Checkout stock deduction race can oversell inventory
Summary
The checkout flow deducts inventory with a non-atomic read-modify-write sequence. Concurrent purchases of the last available unit can both create orders, resulting in oversold stock.
Proof of concept
- Create a product with stock quantity
1.
- Add that product to two different users' carts.
- Send two checkout requests at the same time, for example two concurrent
POST /shop/api/checkout/purchase requests.
- Observe that both checkout paths can create an order even though only one unit was available.
The relevant path is:
shop/payment/providers.py:49 starts order population from checkout.
shop/models/order.py:278 wraps order population in a transaction, but the stock row is not locked.
shop/models/order.py:543 calls cart_item.product.deduct_from_stock().
shop/models/inventory.py:52 performs the stock deduction without an atomic conditional update.
Expected behavior
Only one checkout should succeed when stock quantity is 1. The second concurrent checkout should fail with an out-of-stock response.
Actual behavior
Both checkout requests can pass the stock logic and create orders before the stock change is safely serialized.
Impact
This can oversell inventory and leave orders or stock counts inconsistent with real availability.
Suggested fix
Use row-level locking or an atomic conditional update for stock deduction. For example, inside the checkout transaction, lock the inventory row with select_for_update(), or update stock with a condition such as quantity >= requested_quantity and abort checkout if the update affects zero rows. Add a concurrent checkout regression test for the last unit of stock.
Checkout stock deduction race can oversell inventory
Summary
The checkout flow deducts inventory with a non-atomic read-modify-write sequence. Concurrent purchases of the last available unit can both create orders, resulting in oversold stock.
Proof of concept
1.POST /shop/api/checkout/purchaserequests.The relevant path is:
shop/payment/providers.py:49starts order population from checkout.shop/models/order.py:278wraps order population in a transaction, but the stock row is not locked.shop/models/order.py:543callscart_item.product.deduct_from_stock().shop/models/inventory.py:52performs the stock deduction without an atomic conditional update.Expected behavior
Only one checkout should succeed when stock quantity is
1. The second concurrent checkout should fail with an out-of-stock response.Actual behavior
Both checkout requests can pass the stock logic and create orders before the stock change is safely serialized.
Impact
This can oversell inventory and leave orders or stock counts inconsistent with real availability.
Suggested fix
Use row-level locking or an atomic conditional update for stock deduction. For example, inside the checkout transaction, lock the inventory row with
select_for_update(), or update stock with a condition such asquantity >= requested_quantityand abort checkout if the update affects zero rows. Add a concurrent checkout regression test for the last unit of stock.