Two buyers bought the last item

In this controlled demo, one item is left and two people press Buy at the same moment. Both requests check the stock, both see one, and both orders go through, so the shop sells an item it doesn't have.

Technical name: Check-then-update race on stockControlled demo · fake data

Why did two customers buy the last item?

Because the app checks the stock and subtracts it in two separate steps. When two purchases arrive together, both checks see one item left and both orders go through. In our controlled demo, the last camera got two orders and the stock went to -1. The fix: let the database check and subtract in one step (update only if stock is above zero) and create the order only if that update changed a row.

How to tell if your app has this

No code needed.

  1. Ask your AI tool: "What happens if two people buy the last item at the same time? Show me where we check and change the stock." If the check and the update are separate steps, you have this race.
  2. Look at your orders for items with limited stock. More orders than items, or a stock count below zero, is the symptom.
  3. Test it: set an item's stock to 1 in a test environment, then send two purchase requests at the same moment. One should succeed and one should be rejected.

Before and after

Before
Demo Store · Vintage camera · 1 in stock
aliceOrder confirmed
bobOrder confirmed
Stock-1 (2 orders)

One camera, two orders. Stock went to -1.

After
Demo Store · Vintage camera · 1 in stock
aliceOrder confirmed
bobSold out, no order
Stock0 (1 order)

One order goes through. The other buyer sees sold out.

Why AI-built apps end up with this

The natural way to write a purchase is: read the stock, check it, subtract one. It works every time you test it alone, because a race needs two requests arriving together. AI tools usually write it that way unless you ask about simultaneous buyers.

Fix it yourself

How do I fix it? Paste this into your AI tool

My app checks the stock first and subtracts it later, so two people buying the last item at the same time can both succeed. Change the purchase so the database does the check and the subtraction in one step, for example UPDATE items SET stock = stock - 1 WHERE id = ? AND stock > 0, and only create the order if that update changed exactly one row. If it changed zero rows, show a sold-out message and don't create an order. Do the same for anything else that is limited, like seats, coupons or bookings. Then test it by sending two purchase requests for the last item at the same time: one should succeed and one should be rejected.
Works with Lovable, Bolt, Cursor, Replit and similar tools. Then run the check-up below on your own app.

How we checked the fix

Same request before and after the fix
Same requestBeforeAfter
Two buyers, 1 in stock, buy at the same time2 orders, stock -11 order, stock 0
The second buyerOrder confirmedSold out, no order created

Results from our demo test, run against the same demo before and after the fix.

Common mistakes when fixing this

  • This only shows up when two requests land together, so it passes every test you run alone.
  • Payments need their own plan: reserve the item first, or refund when the reservation fails.

Questions people ask

Does this only affect shops?

No. Anything limited has the same problem: event seats, booking slots, coupon uses, free trial credits or a waitlist spot.

Is a transaction enough?

Not always. A transaction that reads the stock and then writes it can still let two buyers through, depending on the database's isolation level. A conditional update (WHERE stock > 0) or locking the row makes the check and the change one step.

What about the payment?

Plan it separately. Common options are to reserve the item before charging, or to refund automatically when the reservation fails.

For developers: the cause and the fix in code
− if stock > 0:
−     stock = stock - 1
+ UPDATE items SET stock = stock - 1
+ WHERE id = ? AND stock > 0  -- 0 rows = sold out

Simplified. Your stack will look different; the principle is the same.

Learn more