Loading content...
Understanding the lost update problem requires more than definitions—it demands seeing the anomaly unfold in realistic contexts. In this page, we'll walk through comprehensive scenarios across multiple domains, tracing each operation precisely to understand exactly how updates become lost.
These scenarios are not simplified toy examples. They represent the actual patterns that occur in production systems, with enough detail to recognize similar situations in your own work.
For each scenario, we'll: (1) establish the initial state and business context, (2) define the concurrent transactions involved, (3) trace the exact interleaved execution step-by-step, (4) identify the precise moment of failure, and (5) calculate the resulting data corruption.
Business Context:
Maria has a checking account with $500. She is at an ATM withdrawing $100 for dinner. Simultaneously, her landlord's automatic rent deduction of $400 is being processed by the bank's batch system.
The Players:
Initial Database State:
12345678
-- Maria's account in the accounts tableSELECT account_id, holder_name, balance, last_updatedFROM accountsWHERE account_id = 'ACC-7821'; -- Result:-- account_id | holder_name | balance | last_updated-- ACC-7821 | Maria Chen | 500.00 | 2024-12-01 08:00:00Expected Outcome:
If these transactions execute correctly (in any order):
Maria's account should reach $0 regardless of transaction order. Now let's trace what actually happens when the transactions interleave incorrectly:
| Time | T₁ (ATM) | T₂ (Rent) | DB Balance | T₁ Local | T₂ Local |
|---|---|---|---|---|---|
| 09:00:00.000 | BEGIN | — | $500.00 | — | — |
| 09:00:00.005 | — | BEGIN | $500.00 | — | — |
| 09:00:00.010 | SELECT balance → $500 | — | $500.00 | $500.00 | — |
| 09:00:00.012 | — | SELECT balance → $500 | $500.00 | $500.00 | $500.00 |
| 09:00:00.015 | new_balance = $500 - $100 | — | $500.00 | $400.00 | $500.00 |
| 09:00:00.018 | — | new_balance = $500 - $400 | $500.00 | $400.00 | $100.00 |
| 09:00:00.020 | UPDATE balance = $400 | — | $400.00 | $400.00 | $100.00 |
| 09:00:00.022 | — | UPDATE balance = $100 | $100.00 | $400.00 | $100.00 |
| 09:00:00.025 | COMMIT ✓ | — | $100.00 | Done | $100.00 |
| 09:00:00.028 | — | COMMIT ✓ | $100.00 | — | Done |
Final balance: $100.00 instead of $0.00. Maria's $100 ATM withdrawal was effectively reversed! She received $100 cash from the ATM, but her account only decreased by the $400 rent. The bank has lost $100. Maria has received $100 extra. This is fraud-by-accident—no malicious intent, just a concurrency bug.
Critical Moment Analysis:
The lost update occurred because both transactions read the same stale value ($500) before either could write. When T₂ computed its update at 09:00:00.018, it used $500 instead of $400 (T₁'s pending result). This is the fundamental pattern: reads from overlapping time windows create stale-data foundations for writes.
The Audit Trail Paradox:
Making this worse, the transaction log will show both operations:
09:00:00.020 - T₁: UPDATE accounts SET balance = 400 WHERE account_id = 'ACC-7821'
09:00:00.022 - T₂: UPDATE accounts SET balance = 100 WHERE account_id = 'ACC-7821'
An auditor seeing this log will be confused: T₁ set balance to $400, then T₂ set it to $100. The auditor expects $100 final. But both operations should have worked—the log doesn't reveal that T₂ used a stale premise. The audit trail is technically accurate but semantically misleading.
Business Context:
An online electronics retailer launches a flash sale on a popular gaming console. Only 5 units remain in stock. At 10:00 AM, when the sale goes live, 200 customers simultaneously attempt to add the console to their carts and checkout.
The Simplified Scenario:
To illustrate the core problem, let's trace just three concurrent purchase transactions:
Initial Inventory State:
123456789101112
-- Gaming console inventorySELECT sku, product_name, quantity_available, reservedFROM inventoryWHERE sku = 'CONSOLE-X1'; -- Result:-- sku | product_name | quantity_available | reserved-- CONSOLE-X1 | GameBox Pro | 5 | 0 -- Business rule: -- quantity_available must always be >= 0-- Reserved items reduce available countExpected Outcome:
With 5 units and 3 customers, all three should successfully purchase, leaving 2 units remaining:
Problematic Execution:
| Time (μs) | T₁ (Alice) | T₂ (Bob) | T₃ (Carol) | DB Stock | Analysis |
|---|---|---|---|---|---|
| 0 | BEGIN | BEGIN | BEGIN | 5 | All three transactions start simultaneously |
| 100 | SELECT qty → 5 | — | — | 5 | Alice sees 5 available |
| 110 | — | SELECT qty → 5 | — | 5 | Bob also sees 5 available |
| 120 | — | — | SELECT qty → 5 | 5 | Carol also sees 5 available |
| 200 | Check: 5 ≥ 1 ✓ | — | — | 5 | Alice's availability check passes |
| 210 | — | Check: 5 ≥ 1 ✓ | — | 5 | Bob's check passes (same stale data) |
| 220 | — | — | Check: 5 ≥ 1 ✓ | 5 | Carol's check passes (same stale data) |
| 300 | UPDATE qty = 4 | — | — | 4 | Alice decrements correctly |
| 310 | — | UPDATE qty = 4 | — | 4 | Bob overwrites with 5-1=4 (WRONG!) |
| 320 | — | — | UPDATE qty = 4 | 4 | Carol also writes 5-1=4 (WRONG!) |
| 400 | COMMIT ✓ | COMMIT ✓ | COMMIT ✓ | 4 | All succeed, 2 updates LOST |
Database shows 4 units remaining, but 3 units were sold—there should only be 2 units left! Two decrements were lost. Now scale this: with 200 concurrent customers, dozens of updates are lost. The system shows plenty of stock, keeps selling, and the warehouse discovers 50 orders for 5 items.
The Cascading Business Impact:
Why This Pattern is Common:
The read-then-check-then-write pattern is natural for developers:
// Naive implementation - vulnerable to lost updates
const stock = await db.query('SELECT quantity FROM inventory WHERE sku = ?', [sku]);
if (stock.quantity >= requested) {
// DANGER ZONE: Another transaction can modify stock between SELECT and UPDATE
await db.query('UPDATE inventory SET quantity = ? WHERE sku = ?',
[stock.quantity - requested, sku]);
await createOrder(...);
}
The "DANGER ZONE" comment highlights the vulnerable window—the time between reading and writing where other transactions can slip in their own modifications.
Business Context:
A content platform tracks article page views to determine content popularity from revenue sharing with authors. Popular articles mean higher author payments. The view counter is updated every time a user loads an article.
The Pattern:
Counter increments are the most elemental example of read-modify-write:
-- Read current count
SELECT view_count FROM articles WHERE article_id = 'A-123';
-- Result: 1000
-- Modify (application layer)
new_count = 1000 + 1
-- Write updated count
UPDATE articles SET view_count = 1001 WHERE article_id = 'A-123';
This seems straightforward. What could go wrong?
Scenario: Viral Article Peak Traffic:
An article goes viral. During peak hour, 10,000 users view the article within a 10-minute window. If the database receives 1,000 concurrent increment requests and 10% experience lost updates due to timing, 100 views are lost.
Let's trace a specific collision:
| Time | T₁ (User A) | T₂ (User B) | T₃ (User C) | DB Count |
|---|---|---|---|---|
| 0ms | SELECT → 1000 | — | — | 1000 |
| 5ms | — | SELECT → 1000 | — | 1000 |
| 10ms | — | — | SELECT → 1000 | 1000 |
| 15ms | new = 1001 | new = 1001 | new = 1001 | 1000 |
| 20ms | UPDATE = 1001 | — | — | 1001 |
| 25ms | — | UPDATE = 1001 | — | 1001 |
| 30ms | — | — | UPDATE = 1001 | 1001 |
Three users viewed the article. The counter increased by 1 instead of 3. Over time, with thousands of concurrent views, this accumulates to significant under-counting. The author's revenue suffers. The analytics are wrong. Content decisions based on "view counts" are corrupted.
Why Counters Are Especially Vulnerable:
Counter operations have these characteristics that maximize lost update risk:
The Mathematically Correct Counter Value:
Let N = number of increment operations Let C = number of collisions (overlapping R→W windows)
If no lost updates: final_count = initial + N With lost updates: final_count = initial + (N - C)
For high-traffic counters, C can be 5-15% of N without proper concurrency control.
123456789101112131415161718192021222324
-- Analyzing lost updates in view counter-- If lost updates occurred, we can sometimes detect them through rate analysis -- Expected views based on request logsSELECT COUNT(*) as logged_requests FROM request_log WHERE article_id = 'A-123' AND timestamp BETWEEN '2024-01-01' AND '2024-01-31';-- Result: 125,000 requests -- Actual counter incrementSELECT view_count - initial_count as recorded_viewsFROM articles WHERE article_id = 'A-123';-- Result: 108,750 views -- Discrepancy indicates lost updates!-- 125,000 - 108,750 = 16,250 lost updates (13% loss rate) -- This affects:-- 1. Author revenue sharing calculations-- 2. Content recommendation algorithms -- 3. Advertiser metrics and billing-- 4. A/B test statistical validityBusiness Context:
A corporate office uses an internal booking system for conference rooms. The system tracks availability using a boolean is_available flag and a booked_by field.
The Booking Logic (Vulnerable):
12345678910111213141516171819202122232425262728
async function bookRoom(roomId, userId, timeSlot) { // Start transaction await db.beginTransaction(); // Step 1: Read current availability const room = await db.query( 'SELECT is_available, booked_by FROM rooms WHERE room_id = ? AND time_slot = ?', [roomId, timeSlot] ); // Step 2: Check availability if (!room.is_available) { await db.rollback(); return { success: false, message: 'Room already booked' }; } // DANGER: Window between SELECT and UPDATE // Another transaction can complete its booking here! // Step 3: Book the room await db.query( 'UPDATE rooms SET is_available = false, booked_by = ? WHERE room_id = ? AND time_slot = ?', [userId, roomId, timeSlot] ); await db.commit(); return { success: true, message: 'Room booked successfully' };}Concurrent Booking Attempt:
Two managers, Sarah and Tom, both need the large conference room for a 2 PM client meeting. They click "Book Now" at almost exactly the same moment.
| Time | Sarah's Request (T₁) | Tom's Request (T₂) | DB State | Result |
|---|---|---|---|---|
| t₀ | BEGIN | BEGIN | Room: AVAILABLE | Both transactions active |
| t₁ | SELECT → AVAILABLE | — | Room: AVAILABLE | Sarah sees room is free |
| t₂ | — | SELECT → AVAILABLE | Room: AVAILABLE | Tom also sees room is free |
| t₃ | Check: AVAILABLE ✓ | Check: AVAILABLE ✓ | Room: AVAILABLE | Both pass availability check |
| t₄ | UPDATE booked_by = 'Sarah' | — | Room: Sarah | Sarah's booking applied |
| t₅ | — | UPDATE booked_by = 'Tom' | Room: Tom | Tom OVERWRITES Sarah |
| t₆ | COMMIT ✓ | COMMIT ✓ | Room: Tom | Both receive success! |
Sarah received a booking confirmation. Tom received a booking confirmation. At 2 PM, both show up with their clients. The system shows Tom has the room (his update was last). Sarah's calendar shows she booked it. Embarrassment ensues.
Organizational Impact:
This isn't just a scheduling inconvenience:
The fundamental problem: the system gave confirmations that it couldn't honor. Distributed trust was violated.
Business Context:
An airline booking system allows passengers to select specific seats. A flight from New York to London has one first-class seat remaining: 2A, a window seat near the front.
The Race Condition in Seat Selection:
Two frequent flyers, James and Elena, both browse the seat map. Both see seat 2A highlighted as available. Both click to select it. This scenario involves a slightly different lost update pattern—where the "update" is marking a seat as taken.
1234567891011121314151617181920212223242526272829303132333435363738394041424344
def select_seat(flight_id: str, seat_id: str, passenger_id: str): """ Vulnerable seat selection - demonstrates lost update in assignment operations """ # Begin transaction db.begin() try: # Read seat status seat = db.query(""" SELECT seat_id, status, assigned_to FROM seats WHERE flight_id = %s AND seat_id = %s """, (flight_id, seat_id)) # Check if available if seat['status'] != 'AVAILABLE': db.rollback() return SeatSelectionResult( success=False, message=f"Seat {seat_id} is no longer available" ) # TIME GAP: This is where the lost update window exists # Another transaction can read AVAILABLE and proceed # Mark seat as taken db.execute(""" UPDATE seats SET status = 'ASSIGNED', assigned_to = %s, assigned_at = NOW() WHERE flight_id = %s AND seat_id = %s """, (passenger_id, flight_id, seat_id)) db.commit() return SeatSelectionResult( success=True, message=f"Seat {seat_id} assigned to {passenger_id}", seat=seat_id ) except Exception as e: db.rollback() raise| Time | James (T₁) | Elena (T₂) | Seat 2A DB State |
|---|---|---|---|
| 10:30:00.000 | BEGIN | BEGIN | status=AVAILABLE, assigned_to=NULL |
| 10:30:00.010 | SELECT → AVAILABLE | — | status=AVAILABLE, assigned_to=NULL |
| 10:30:00.015 | — | SELECT → AVAILABLE | status=AVAILABLE, assigned_to=NULL |
| 10:30:00.020 | CHECK passes ✓ | — | status=AVAILABLE, assigned_to=NULL |
| 10:30:00.025 | — | CHECK passes ✓ | status=AVAILABLE, assigned_to=NULL |
| 10:30:00.050 | UPDATE → James | — | status=ASSIGNED, assigned_to=James |
| 10:30:00.060 | — | UPDATE → Elena | status=ASSIGNED, assigned_to=Elena |
| 10:30:00.100 | COMMIT ✓ | COMMIT ✓ | status=ASSIGNED, assigned_to=Elena |
Both passengers received confirmation that seat 2A was theirs. James's boarding pass shows 2A. Elena's boarding pass shows 2A. The database only has one record—Elena's (she committed last). At the gate, chaos.
The Gate Agent Nightmare:
At boarding time:
Why This Pattern is Pernicious:
The "assignment" operation seems like a simple write, but it's actually a conditional write predicated on reading the current state. The logic is:
IF current_state == AVAILABLE:
new_state = ASSIGNED to ME
When two transactions execute this logic concurrently with interleaved reads and writes, one assignment is lost. The seat cannot belong to both passengers—one will be disappointed.
Across all five scenarios, the same fundamental pattern emerges. Recognizing this pattern in your own code is essential for prevention.
| Scenario | Data Item (X) | Read Value (X₀) | Computation | Conflict Result |
|---|---|---|---|---|
| Banking | Account balance | $500 | balance - withdrawal | $100 lost (should be $0) |
| E-commerce | Inventory count | 5 units | count - 1 | 2 sales lost (shows 4, should be 2) |
| Page Views | View counter | 1000 | count + 1 | 2 views lost (shows 1001, should be 1003) |
| Booking | Room availability | AVAILABLE | BOOKED by user | 1 booking lost (double confirmation) |
| Seat Selection | Seat assignment | NULL/Available | assigned_to = passenger | 1 assignment lost (duplicate seat) |
Whenever you see a pattern where code SELECTs a row, uses those values in application logic, then UPDATEs the same row—without any locking or optimistic concurrency control—you are looking at a potential lost update vulnerability. This pattern should trigger immediate review.
We've explored five detailed scenarios demonstrating the lost update problem across different domains. These aren't contrived academic exercises—they represent the actual failure modes that occur in production systems every day.
What's Next:
Now that we've seen how lost updates manifest in realistic scenarios, the next page examines the deeper consequences: how lost updates corrupt data integrity, violate business invariants, and compound into systemic data quality problems over time.
You can now recognize lost update patterns in real-world scenarios and trace their execution step-by-step. This practical understanding is essential for identifying vulnerable code in your own systems before the anomaly manifests in production.