Understanding the Lost Update Problem
Example Scenario
Read aloud is unavailable in this browser.
Bringing Theory to Life
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.
Scenario 1: The Double Withdrawal
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:
- Transaction T₁ (ATM Withdrawal): Deducts $100 from Maria's account
- Transaction T₂ (Automatic Rent Payment): Deducts $400 from Maria's account
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):
- Initial: $500
- After T₁: $500 - $100 = $400
- After T₂: $400 - $400 = $0
- OR After T₂ then T₁: $500 - $400 - $100 = $0
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.
Scenario 2: The Flash Sale Disaster
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:
- Transaction T₁ (Customer Alice): Purchasing 1 unit
- Transaction T₂ (Customer Bob): Purchasing 1 unit
- Transaction T₃ (Customer Carol): Purchasing 1 unit
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:
- Initial: 5 units
- After T₁, T₂, T₃: 5 - 1 - 1 - 1 = 2 units
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:
- Customer Service Crisis: 45+ customers receive "out of stock" emails days after receiving confirmation
- Refund Processing: Finance must process refunds, potentially with additional compensation
- Marketing Damage: Social media complaints about "bait and switch" tactics
- Competitor Advantage: Frustrated customers go to competitors for future purchases
- Legal Risk: In some jurisdictions, selling unavailable inventory is consumer fraud
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.
Scenario 3: The Vanishing Page Views
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:
- High Frequency: Popular resources receive many updates per second
- Simple Pattern: Every operation follows the same R→M→W sequence
- Same Computation: Every operation adds the same value (usually 1)
- No Business Logic Delay: Unlike orders that require validation steps, counters are fast—so more can collide
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 validityScenario 4: The Double-Booked Conference Room
Business 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.
- •Searched for available rooms at 2 PM
- •Large Conference Room showed "AVAILABLE"
- •Clicked "Book Now"
- •Received email: "Your room is booked"
- •Meeting added to her calendar
- •At 2 PM: Room occupied by Tom's meeting
- •Searched for available rooms at 2 PM
- •Large Conference Room showed "AVAILABLE"
- •Clicked "Book Now"
- •Received email: "Your room is booked"
- •Meeting added to his calendar
- •At 2 PM: Room is his (he "won")
Organizational Impact:
This isn't just a scheduling inconvenience:
- Client Perception: External clients witness internal chaos—unprofessional appearance
- Meeting Disruption: Sarah's meeting either starts late or happens in an inferior room
- Trust Erosion: Employees stop trusting the booking system; resort to paper signup sheets
- Help Desk Load: IT receives complaints about a "buggy" booking system
- Executive Attention: If high-profile meetings are affected, the issue escalates
The fundamental problem: the system gave confirmations that it couldn't honor. Distributed trust was violated.
Scenario 5: The Phantom Seat Selection
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:
- James presents boarding pass showing seat 2A
- Elena presents boarding pass showing seat 2A
- System shows Elena in 2A
- James's confirmation email proves he selected the seat
- Gate agent must manually re-seat one passenger
- First class is full; James is downgraded
- Compensation required; customer loyalty damaged
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.
Recognizing the Lost Update Pattern
Across all five scenarios, the same fundamental pattern emerges. Recognizing this pattern in your own code is essential for prevention.
- •Read Phase: Transaction reads data item X, obtaining value X₀
- •Computation Phase: Transaction computes new value based on X₀ (e.g., X₀ - 100, X₀ + 1, or "assign to me")
- •Write Phase: Transaction writes computed value to X
- •Collision: Another transaction performed the same R→C→W cycle during the window, using the same X₀
- •Lost Update: One transaction's write overwrites the other's, as if the first never happened
| 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.
Summary: Scenarios and Patterns
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.
- •Lost updates manifest across all domains: Banking, e-commerce, analytics, booking systems, and reservations all share the same vulnerability pattern.
- •The pattern is always R→M→W: Read current state, compute new state, write computed state. When two transactions interleave, one write clobbers the other.
- •Both transactions appear successful: No errors, no crashes, no warnings. This silent failure makes lost updates uniquely dangerous.
- •The impact scales with concurrency: Low-traffic systems may never experience the problem. High-traffic systems lose updates continuously.
- •Business consequences are severe: Financial losses, overselling, incorrect analytics, double-bookings, and customer trust erosion.
- •Detection is non-trivial: Without external reference data (like request logs), you may not even know updates are being lost.
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.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.