← Blog
#discuss
Israel
Israel
Full-Stack Developer · Posted on Sep 8

The race condition that shipped two orders for one seat

Two checkout requests landed in the same millisecond. The read-then-write check in between wasn't atomic, and the database let both through.

The bug report was simple: a customer got charged for a seat that had already been sold to someone else. The code that checked availability looked correct — read the seat's status, confirm it's open, write 'sold'.

The problem was the gap between the read and the write. Under enough concurrent load, two checkout requests read 'available' before either one had written 'sold'. Both proceeded. Both charged a card.

The fix wasn't more validation in the application layer — validation running against a stale read is still validation against a stale read, no matter how many checks you stack on top of it. The fix was pushing the check into the same statement as the write, using a conditional update with `WHERE status = 'available'` and checking the row count that came back.

If zero rows were affected, the seat was already gone, and the app could return a clean 'sold out' instead of a duplicate charge. Postgres's row-level locking made the check-and-set atomic — no window for a second request to slip through.

The broader lesson: any 'check, then act' logic that spans two separate database round-trips has a race condition in it by default. The question isn't whether it's a bug — it's whether traffic is high enough yet to notice.

0 comments11 views7 min read

Comments

No comments yet — be the first to say something.