Founder & Lead Engineer, RAITHub
RAITHub ships and tests production software. See QA as a Service or talk to us.
When you have oversold, do two things in order: stop new oversells, then clean up the orders you cannot fulfil. Set the affected variants to zero immediately. Then query your orders against real stock to list every affected order, decide who to honour by order time, tell those buyers plainly, and refund the rest. Only after that, fix the race condition so it cannot recur.
This is the incident playbook. The underlying engineering fix, with the full SQL, is in stopping overselling with stock reservation. If you want the incident handled and the cause fixed for you, see how RAITHub would fix this below.
What should you do in the first hour of an oversell incident?
Contain it before you clean up, so the list of affected orders stops growing while you work.
- Take the affected items off sale. Set the oversold variants to zero or mark them unavailable. This stops new orders landing on stock that is already gone.
- Freeze automatic fulfilment for those items, so a warehouse or 3PL does not ship against orders you are about to cancel.
- Snapshot the orders and stock tables before you change anything, so you have a record of the exact state when you found it.
- Name one owner. One person decides who is honoured and what buyers are told, so the message is consistent.
- Do not change prices or delete orders yet. You need the full picture first, and deleting orders destroys the evidence of who ordered what, when.
How do you find every affected order?
From the data, not by eye. Compare what you have promised against what you actually have, per variant. A query that sums open order quantities per variant and subtracts physical stock gives you the exact shortfall and the orders behind it.
-- Variants where promised quantity exceeds real stock, worst first.
SELECT v.id AS variant_id,
v.sku,
v.stock,
SUM(ol.qty) AS promised,
SUM(ol.qty) - v.stock AS short_by
FROM order_lines ol
JOIN orders o ON o.id = ol.order_id
JOIN product_variants v ON v.id = ol.variant_id
WHERE o.status IN ('placed', 'paid') -- not yet shipped or cancelled
GROUP BY v.id, v.sku, v.stock
HAVING SUM(ol.qty) > v.stock
ORDER BY short_by DESC;
Then list the orders for each short variant in the order they were placed, so you can apply a fair rule. Honouring by order time (first placed, first served) is the rule buyers accept most easily and the easiest to defend.
-- Orders competing for one short variant, oldest first.
SELECT o.id, o.created_at, ol.qty, o.customer_id, o.status
FROM order_lines ol
JOIN orders o ON o.id = ol.order_id
WHERE ol.variant_id = $1
AND o.status IN ('placed', 'paid')
ORDER BY o.created_at; -- fill from the top until stock runs out
How do you decide who to honour, and what to tell them?
Pick a rule, apply it to everyone, and communicate before buyers chase you. The table below covers the common cases.
| Situation | What to do | What to tell the buyer |
|---|---|---|
| Order can be filled from real stock | Ship as normal | Nothing special; proceed |
| Paid, but no stock to fill it | Refund in full promptly | Apologise, explain the item sold out, confirm the refund and when it lands |
| Restock expected soon | Offer the choice: wait for restock or refund now | Give a realistic date and a one-click way to choose |
| Partial order (one line short) | Ship what you have, refund the missing line | Explain which item is refunded and why |
Refund quickly even where you could stall, because a fast refund and a clear message is what keeps the customer. Where money moves, make sure the refund is recorded against the order and reconciled, so your finance records match the gateway. If the incident is large, a short public note (an item sold out faster than stock updated, affected orders are being refunded) stops the same complaint arriving a hundred times.
Why did the oversell happen?
Almost always a race condition: two orders read the same stock count, both decided there was enough, and both wrote. Nothing throws an error, so both buyers get a confirmation and the shortfall appears later. It gets more likely under exactly the conditions you care about, a sale or a popular item, because that is when orders overlap. Checking stock in the browser or in an earlier request does not help, because another order can slip in between the check and the write.
| Step | Order A | Order B | Stock in the database |
|---|---|---|---|
| 1 | Reads stock: 1. Enough. | 1 | |
| 2 | Reads stock: 1. Enough. | 1 | |
| 3 | Writes stock = 0, creates order | 0 | |
| 4 | Writes stock = 0, creates order | 0, two orders for one unit |
How do you stop it happening again?
Make the check and the write one atomic step, inside the order transaction. A conditional update only decrements when there is enough stock, and tells you whether it changed a row.
UPDATE product_variants
SET stock = stock - $2
WHERE id = $1
AND stock >= $2
RETURNING stock;
If it updates one row, the units are yours. If it updates none, the item is gone and the order rolls back. Under PostgreSQL's default Read Committed isolation, the second order's update waits for the first to commit, then re-evaluates its WHERE clause and finds stock is now 0, so it updates nothing (PostgreSQL, transaction isolation). Add a floor so a future code path cannot go negative:
ALTER TABLE product_variants
ADD CONSTRAINT stock_not_negative CHECK (stock >= 0);
Keep stock per variant, not per product, so you cannot sell mediums you do not have while larges are in stock. The full order transaction, deadlock-safe line ordering, and when to reserve stock during payment are all in the stock reservation guide.
How do you prove it cannot recur?
With a concurrency test against a real database, not a mock. Set stock to 1, place two orders at once, and assert exactly one succeeds.
it('sells the last unit exactly once', async () => {
await setStock(variantId, 1)
const results = await Promise.allSettled([
placeOrder(customerA, [{ variantId, qty: 1 }]),
placeOrder(customerB, [{ variantId, qty: 1 }]),
])
expect(results.filter((r) => r.status === 'fulfilled')).toHaveLength(1)
expect(await getStock(variantId)).toBe(0)
})
Run the same test with the fix removed and it should fail some of the time; that is how you know it measures the race and not luck. Keep it in CI so the next change to the order path cannot bring the oversell back.
Buy, build or hire?
| Option | What you get | Choose this when |
|---|---|---|
| Platform inventory settings | Stock managed by your platform's own engine | You are on a hosted platform; configure its inventory and overselling settings |
| A one-off incident fix | The affected orders found and the race condition closed | You oversold once and need the cause fixed and tested, not a rewrite |
| Fix it with your own team | Full control, if they can write the transaction and the concurrency test | The order path is in a codebase your team knows |
| A custom build or rescue | Transaction-safe stock across the whole order flow | Overselling is one of several money-correctness problems in the store |
How RAITHub would fix this
- Scope: stop new oversells, query the affected orders and the shortfall, apply a fair honour rule, move the stock decrement inside the order transaction with a conditional update and a floor, add a concurrency test, and gate it in CI.
- Timeline: an oversell incident and its fix sit inside the code rescue range of 2 to 4 weeks; a wider stock and money rework is scoped separately.
- What you receive: the list of affected orders, the transaction-level fix, the concurrency test in your CI, a short incident write-up, the IP assigned to you and an NDA as standard.
- Next step: a free 15-minute technical audit, then a written fixed quote.
RAITHub built and runs TheSkinProof, the founder's own marketplace venture, which decrements per-variant stock inside the order transaction across four payment rails, with 217 API endpoints and 750+ tests. For the data-side detail of a parallel incident, migrating products and orders safely covers keeping stock correct through a migration. See the eCommerce industry page for context, read about a custom build, or book the free 15-minute audit with your order flow and the affected variants.
Documentation checked on 11 October 2026.
Frequently asked questions
What do I do right now if I oversold stock?
Take the affected items off sale so no new orders land, freeze automatic fulfilment for them, and snapshot your orders and stock tables. Then list the affected orders from the data, decide who to honour by order time, refund the rest promptly, and tell every affected buyer plainly.
How do I find which orders I cannot fulfil?
Query your open orders against real stock per variant: sum the ordered quantity for each variant and subtract physical stock. Any variant where the promised quantity exceeds stock is oversold, and the orders behind it, listed oldest first, are the ones to triage.
Who should I honour when I have oversold?
Apply one rule to everyone. Honouring by order time, first placed first served, is the fairest and easiest to defend. Fill orders from the oldest until real stock runs out, then refund the rest in full and quickly, with a clear apology and an expected refund date.
Why does overselling happen even when I check stock?
Because the check and the write are two separate steps, and a second order can slip in between them. Both orders read the same count, both decide there is enough, and both write. The only fix is to make the check and the decrement one atomic step inside the order transaction.
How do I stop overselling permanently?
Decrement stock inside the order transaction with a conditional update such as UPDATE ... SET stock = stock - $2 WHERE id = $1 AND stock >= $2, add a CHECK (stock >= 0) floor, and keep stock per variant. Then prove it with a concurrency test that places two orders for the last unit and asserts exactly one wins.
Does my hosted platform prevent overselling for me?
Mainstream hosted platforms manage stock in their own engine, so configure their inventory and overselling settings rather than building your own. The transaction-level fix in this post is for custom stores and marketplaces that create orders in their own database.
Related posts
Ready to discuss your project?
Book a free 15-minute technical audit with our engineering team.