Skip to content

Checkout stock deduction race can oversell inventory #888

Description

@Galaxync

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. Create a product with stock quantity 1.
  2. Add that product to two different users' carts.
  3. Send two checkout requests at the same time, for example two concurrent POST /shop/api/checkout/purchase requests.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions