Concurrency Animation

The Last Seat

Two travelers race for seat 14C on flight ZZZ 270. Watch the same checkout run with a lock, and then without one.

User A
San Francisco
https://book.zuelair.zzz/seat/14C
Zuel AirlinesZZZ 270 · SFO → ZuelPort
Seat 14C · Window
Extra legroom
$49
Loading availability…
Booking System
Seat lock: none
Database · seat 14C
AVAILABLE
One shared record. Every purchase should check it, then claim it.
User B
Chicago
https://book.zuelair.zzz/seat/14C
Zuel AirlinesZZZ 270 · SFO → ZuelPort
Seat 14C · Window
Extra legroom
$49
Loading availability…
t₁
t₂
t₃
t₄
Protected — locked
The system serializes access: the seat is claimed atomically before anyone else can read it.

Why the second run breaks

Both scenarios run the identical two operations — check whether 14C is free, then claim it. The only difference is whether anything stops another request from slipping between those two steps.

In the protected run the system takes a lock, so check-and-claim behave as one indivisible step. B's request is evaluated against the record as it actually is, and is correctly refused.

In the racing run there is no lock. B reads the record at t₂, before A commits at t₃. B's answer was true when it was given and false by the time it was used — the classic time-of-check to time-of-use gap. Two confirmations are issued for one seat.

The fix is not "check again" — a second check has the same gap. It is to make the check and the write atomic: a row lock, a conditional update that only succeeds while the seat is still free (UPDATE … WHERE status = 'available'), or a uniqueness constraint that makes the second booking impossible to store.

For the operating-system form of the same bug — a privileged binary racing a symlink swap between stat() and open() — see Race Conditions.