Preventing Overbooking When Two Sessions Grab the Last Seat

Two customers click the last available seat at nearly the same moment. Preventing overbooking requires the database to decide which transaction wins, not two separate reads that both say yes.

Two forks, one red, pressing into the last slice of pie at the same moment

Reproduce the Race Safely

Use a disposable table in a test database with one available seat. In two query windows, begin transactions and read the same inventory value before either window writes. Both sessions can see one seat. If each then inserts a booking based on that earlier read, both can succeed unless a constraint or locking design stops them. The demo is about interleaving, so run the reads first, pause, then run the writes. I ask teams to do this once with two windows. The race becomes much easier to understand than a paragraph about isolation levels.

CREATE TABLE dbo.SeatInventory
(
    EventID int NOT NULL PRIMARY KEY,
    SeatsLeft int NOT NULL CONSTRAINT CK_SeatInventory_Nonnegative CHECK (SeatsLeft >= 0)
);
INSERT dbo.SeatInventory(EventID, SeatsLeft) VALUES (1, 1);
SELECT SeatsLeft FROM dbo.SeatInventory WHERE EventID = 1;

Let One Statement Choose the Winner

The simplest fix is a conditional UPDATE that decrements only when SeatsLeft is positive. SQL Server takes the required write locks while executing that statement. Check @@ROWCOUNT immediately. One row means the reservation succeeded; zero means no seat was available. Run the block a second time and it stops with error 50010, because the only seat is already gone. Put the inventory change and booking insert in the same transaction so a later insert failure rolls the decrement back. I prefer this pattern when inventory is represented by a counter. It is compact, testable, and avoids a separate stale read.

BEGIN TRANSACTION;
UPDATE dbo.SeatInventory
SET SeatsLeft = SeatsLeft - 1
WHERE EventID = 1 AND SeatsLeft > 0;
IF @@ROWCOUNT = 0
BEGIN
    ROLLBACK TRANSACTION;
    THROW 50010, 'No seat is available.', 1;
END;
COMMIT TRANSACTION;

Lock the Read When Logic Needs It

Some booking rules require several checks before the write. In that case, read the inventory row with UPDLOCK and HOLDLOCK inside a short transaction. UPDLOCK reserves the intent to update, while HOLDLOCK keeps serializable semantics for the read. Use a predicate supported by an appropriate index so the lock scope is narrow. Keep user interaction outside the transaction. A transaction waiting for a person to choose a meal is a fine way to reserve the entire restaurant by accident.

I check the lock behavior with two windows, not by reading the query text and hoping. Under read committed snapshot isolation, an ordinary read sees the last committed version without waiting; the explicit locking pattern changes that behavior for this critical decision.

BEGIN TRANSACTION;
SELECT SeatsLeft
FROM dbo.SeatInventory WITH (UPDLOCK, HOLDLOCK)
WHERE EventID = 1;
-- Validate the booking rule, then update and insert before COMMIT.
ROLLBACK TRANSACTION;

Preventing Overbooking With Constraints

A CHECK constraint on SeatsLeft >= 0 is a final guard against an oversold counter. It does not replace the conditional update, because a failed write still needs a clear response and transaction handling. If every seat has a distinct identifier, a unique constraint on EventID and SeatID can prevent two bookings for the same seat. Design the constraint around the true business invariant. A count constraint cannot prove that no two customers share one numbered seat.

Do not rely on an application-side check alone. Applications scale across processes, and a read from one process is stale as soon as another commits. The database is the shared point that can enforce the rule.

Two reads, or one statement that decides: a diagram about the preventing overbooking

Preventing Overbooking Across Both Sessions

Run simultaneous attempts against one remaining seat. Verify one success, one clear failure, one inventory decrement, and one booking row. Repeat after a forced insert failure to confirm the decrement rolls back. Test a retry with the same operation ID so a duplicate request does not book twice. Network retries are common; a booking API needs an idempotency rule as well as concurrency control. I also check deadlock behavior under load and make the caller retry only the whole transaction when appropriate.

What does the losing customer see? Return a clear "sold out" response rather than a raw constraint error. The constraint protects the data, while the procedure gives the user a sensible outcome.

Separate Capacity From Individual Seats

The counter pattern works when any remaining seat is interchangeable. Numbered seats require a row-level uniqueness rule on the actual seat identifier. A theater can have one seat left in its counter while two requests ask for the same seat. I represent each seat or reservation with a unique key and enforce one active booking per seat through a constraint or carefully designed filtered unique index. A counter can still summarize availability, but it cannot be the sole source of truth for assigned seats.

For general inventory, a conditional decrement protects the quantity. For one specific resource, a unique reservation key protects the identity. I choose the invariant before writing the transaction. Otherwise, the code can be race-free for the wrong thing. The two-window test should target the same seat, not merely the same event.

Handle Cancellation and Retry

A booking request can time out after the database commits but before the application receives the response. If the caller retries, a second conditional decrement can book twice unless the operation has an idempotency key. Store a unique request ID with the booking and return the existing outcome when that ID is seen again. Keep the uniqueness check and inventory change in one transaction. I test both "two different customers race" and "one customer repeats the same request." They are different concurrency problems.

What should happen when payment fails after a seat is reserved? Decide whether the reservation expires or is explicitly released. Use a reliable state transition and an audit trail. Do not simply increment the counter in an error handler without checking whether the booking was ever committed. Compensation must be as carefully guarded as the original decrement, or the recovery path can create seats that never existed.

Preventing Overbooking Without Long Locks

Put price lookup, email sending, and payment gateway calls outside the lock-held part where possible. Decide the order of payment authorization and inventory reservation with the business owner, including release of a reservation that expires. An atomic SQL update solves the last-seat counter race; it does not design the full commerce workflow. Document timeout and compensation rules.

I watch for blocking on the inventory key after deployment. A hot event can make one row the natural serialization point. That serialization is correct, but preventing overbooking on a hot event still needs a throughput test. If contention grows, introduce a bounded reservation queue or seat-specific rows rather than weakening the invariant.

Related reading on this blog: UPDLOCK and READPAST for Queue Tables and SQL Server Deadlock: Build One With Your Own Hands.

Does this guard stop overbooking?: a checklist on the preventing overbooking

A seat check is not a reservation, it is only a momentary observation.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.

SQL Constraint and Keys, SQL Lock, SQL Server, SQL Transactions, Transaction Isolation
Previous Post
SQL SERVER – Fix: Error 15372 Failed to generate a user instance of SQL Server due to a failure in starting the process for the user instance – The connection will be closed
Next Post
SQL SERVER – 2008 – Customize Toolbar – Remove Debug Button from Toolbar

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.