Two Users. One Seat. Who Wins?
Imagine you and Elon Musk are both trying to board the last seat on a flight to San Francisco for a sold-out Sam Altman keynote.
Two Users. One Seat. Who Wins?

Imagine you and Elon Musk are both trying to board the last seat on a flight to San Francisco for a sold-out Sam Altman keynote.
Both of you booked at the same time. Both got a confirmation email. Both show up at the gate with valid tickets.
Now look, you could probably negotiate something. Let Elon through in exchange for a selfie, maybe an autograph, maybe he names a satellite after you.
Tempting deal honestly…
But the seat issue is still an issue. The airline oversold it.
Two confirmed bookings. One seat. Somebody has to deal with that.
This exact scenario happens in backend systems every single day.
Two users hit your booking endpoint at the same millisecond. Both requests read the database. Both see one seat available. Both write a confirmed booking. Your code did exactly what it was supposed to do — and you still ended up with a problem.
This is called a race condition.
And here’s the sneaky part: it never shows up when you’re testing alone on your laptop. You test the booking flow, it works perfectly, you ship it. Then real users show up, traffic spikes, two people click “Book” at the same millisecond, and suddenly you have a very angry customer on hold with your support team.
Race conditions don’t fail consistently. They fail probabilistically — meaning the more users you have, the more likely they are to hit you. A feature can work perfectly in staging, perfectly in production for months, and then one busy Friday evening it silently breaks.
Why does this happen at all?
Think of your database like a shared whiteboard in an office.
Person A walks up, reads “Seats Available: 1”, and starts writing “Seats Available: 0.”
But before they finish writing, Person B also walks up, reads the board while it still says 1, and also starts writing “Seats Available: 0.”
Both of them think they got the last seat. Both of them are wrong. The whiteboard has no idea two people were writing on it at the same time.
This is exactly what happens when two database reads happen before either write has completed. The database isn’t broken. It’s just doing what you told it to do — and you never told it to check whether someone else was already in the middle of doing the same thing.
When this goes wrong in the real world, it hurts.
A flash sale e-commerce site oversells 200 units of a limited-edition product because their inventory check and order creation weren’t wrapped in a transaction. They have to manually cancel and refund hundreds of orders. Their trust rating tanks overnight.
A fintech startup lets users transfer the same balance twice because two transfer requests fired simultaneously and both passed the “sufficient funds” check before either deduction was written. Thousands of dollars effectively disappear from their ledger.
A ticketing platform — you can probably guess this one — sells the same concert seat to two different people. Two fans show up. One goes home.
None of these teams wrote bad code on purpose. They just didn’t account for what happens when two users show up at exactly the same moment.
The fix is a database transaction with row locking.
BEGIN;
SELECT * FROM seats WHERE id = 1 FOR UPDATE;
UPDATE seats SET booked = true WHERE id = 1;
COMMIT;
That FOR UPDATE is the key piece. It locks the row the moment the first request reads it. The second request comes in, tries to read the same row, and has to wait. It literally cannot proceed until the first transaction either commits or rolls back.
By the time the second request gets access, the seat is already taken. It sees booked = true, and the booking fails cleanly. One confirmation email goes out. One passenger boards. No awkward gate negotiation. No satellite named after you.
Here’s what that looks like with Prisma in a Node.js backend:
await prisma.$transaction(async (tx) => {
const seat = await tx.seat.findFirst({
where: { id: 1, booked: false },
});
if (!seat) throw new Error("Already booked");
return tx.seat.update({
where: { id: 1 },
data: { booked: true },
});
});
Everything inside that block runs as a single atomic operation. Either all of it succeeds, or none of it does. If anything goes wrong — a crash, a network hiccup, a thrown error — Postgres rolls everything back automatically. No half-finished state sitting silently in your database waiting to cause problems at 2am.
One more thing worth knowing: optimistic vs pessimistic locking.
What we just covered is called pessimistic locking — you assume conflict is likely, so you lock the row upfront and make everyone else wait.
There’s another approach called optimistic locking, where you assume conflict is unlikely, so you don’t lock anything. Instead, you add a version field to your row. When you go to update, you check that the version hasn't changed since you read it. If it has, someone else got there first — you retry or return an error
UPDATE seats
SET booked = true, version = version + 1
WHERE id = 1 AND version = 3 AND booked = false;
If zero rows are updated, you know a conflict happened.
Optimistic locking is better for high-read, low-write situations where conflicts are rare. Pessimistic locking is better when conflicts are genuinely likely — like ticketing or flash sales where hundreds of users are hitting the same row at the same time.
Knowing which one to reach for, and why, is the kind of judgment that separates a developer who ships reliable systems from one who ships systems that mostly work.
This is one of those topics that falls through the cracks everywhere.
Too specific for beginner content. Not trendy enough to get a Twitter thread. So most developers only learn it when something breaks in production and they’re scrambling to understand why their perfectly-written code just oversold 300 concert tickets.
That’s why I put together a course covering exactly this kind of thing. Real problems that show up in real codebases, explained before they get a chance to bite you. Race conditions, transaction management, failure modes, the stuff that doesn’t make it into tutorials but absolutely makes it into production incidents.
Anyway — had you come across race conditions before, or was this the first time?
Reply and let me know 👀
메타데이터
- post_id
- 6d58c7fb1bd1
- slug
- two-users-one-seat-who-wins-6d58c7fb1bd1
- url
- https://medium.com/@technicalrupesh13/two-users-one-seat-who-wins-6d58c7fb1bd1
- canonical_url
- https://medium.com/@technicalrupesh13/two-users-one-seat-who-wins-6d58c7fb1bd1
- author_url
- https://medium.com/@technicalrupesh13
- status
- ok
- fetched_at
- 2026-06-09 15:37:30