{"post":{"slug":"database-isolation-levels-2026","title":"Database Isolation Levels: The Bugs You Only Get Under Load","excerpt":"Your code is correct when one user runs it. Two users at once produce a result neither would accept. Isolation levels decide which of those anomalies you get.","category":"code","date":"August 12, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">T</span>he code reads a balance, checks it is sufficient, subtracts an amount, and writes it back. It is obviously correct. It passes every test. It has been in production for a year.\n\nThen two requests arrive within a few milliseconds of each other. Both read a balance of 100. Both check that 100 is enough for a withdrawal of 80. Both write back 20. Two withdrawals of 80 happened; the account lost 80.\n\nNothing is broken in the usual sense. The database did exactly what the isolation level permits, and the isolation level is almost certainly the default you never chose.\n\n## What changed in 2026\n\n- **Managed databases made the default less visible.** Fewer teams configure isolation explicitly, so more teams inherit Read Committed without knowing what it allows.\n- **Serverless and edge compute raised concurrency.** More concurrent connections against the same rows means anomalies that were once rare became routine.\n- **Optimistic concurrency spread.** Version columns and compare-and-swap updates became a common alternative to raising isolation levels.\n- **Retry logic became expected.** Serializable isolation without retry handling is a known incomplete implementation rather than a working configuration.\n\n## What each level allows\n\n| Level | Dirty read | Non-repeatable read | Phantom | Lost update | Write skew |\n| --- | --- | --- | --- | --- | --- |\n| Read Uncommitted | Possible | Possible | Possible | Possible | Possible |\n| Read Committed | No | Possible | Possible | Possible | Possible |\n| Repeatable Read | No | No | Varies | Prevented in some engines | Possible |\n| Serializable | No | No | No | No | No |\n\nThe row worth staring at is Read Committed, because it is the default in several widely-used databases and it permits the lost update above.\n\nThe anomalies, concretely:\n\n**Non-repeatable read** — you read a row, someone commits a change, you read it again in the same transaction and get different data. Calculations spanning both reads are inconsistent.\n\n**Phantom read** — you run a query, someone inserts a matching row, you run it again and the result set grew. Breaks anything that counts or aggregates twice.\n\n**Lost update** — two transactions read, modify, and write the same row. The second overwrites the first. This is the balance example and it is the most common real-world instance.\n\n**Write skew** — the subtle one. Two transactions read overlapping data, check a constraint that holds for each individually, and write to *different* rows. Neither conflicts directly; the combined result violates the invariant. The classic case is two doctors both going off-call because each checked that at least one other doctor was on call.\n\n## Fixing it without going serializable\n\nRaising the global isolation level is the blunt instrument. It works and it slows everything down to solve a problem that usually exists in a handful of transactions.\n\n**Explicit row locks.** `SELECT ... FOR UPDATE` takes a lock on the rows you read, so a concurrent transaction blocks until you commit. This solves lost updates directly and targets only the transaction that needs it. The cost is contention — everyone touching those rows serialises — so keep the transaction short and lock in a consistent order across your codebase to avoid deadlocks.\n\n**Atomic operations.** Instead of read-modify-write, express the change as a single statement: `UPDATE accounts SET balance = balance - 80 WHERE id = ? AND balance >= 80`. The database evaluates and applies atomically, and the affected-row count tells you whether it succeeded. This is the cleanest fix when the operation can be expressed that way, and it needs no locks or retries.\n\n**Optimistic concurrency.** Add a version column. Read it, and on write require the version to be unchanged: `UPDATE ... WHERE id = ? AND version = ?`. Zero rows affected means someone else got there first, and you retry. Excellent under low contention, wasteful under high.\n\n**Serializable, where the invariant genuinely spans rows.** Write skew is the case that locks do not fix, because there is no single row to lock. When correctness depends on a condition across a set, serializable is the honest answer — paired with retry handling, because it works by aborting transactions that would violate serial ordering.\n\n## Common mistakes\n\n- **Assuming a transaction is enough.** A transaction gives atomicity. Isolation is a separate setting and a separate guarantee.\n- **Read-modify-write without a lock or version check.** The single most common concurrency bug in application code.\n- **Serializable without retry logic.** Transactions abort by design; unhandled, users see errors.\n- **Raising isolation globally for one endpoint.** Everything pays; set it per transaction.\n- **Long transactions.** Holding locks across a network call or a slow computation converts a correctness fix into an availability problem.\n- **Inconsistent lock ordering.** Two transactions locking A-then-B and B-then-A deadlock.\n- **Testing only single-user.** These bugs are invisible without concurrency in the test.\n\n## FAQ\n\n**Which level should I default to?**\nRead Committed is a reasonable default for most applications, with explicit locks or atomic updates on the specific transactions that need more. Raising the global default is rarely the right first move — it slows every query to fix a few.\n\n**Does Repeatable Read prevent lost updates?**\nIt depends on the engine, which is exactly why this is confusing. Some detect the conflict and abort; others allow it. Check your database's documented behaviour rather than the standard's name, because implementations diverge meaningfully here.\n\n**Do read replicas change this?**\nThey add their own wrinkle — a replica may lag, so a read-after-write against a replica can return stale data regardless of isolation level. That is a replication concern layered on top of isolation. See [read replicas explained](/blog/read-replicas-explained-2026).\n\n**What about NoSQL databases?**\nGuarantees vary widely; many offer per-document atomicity without multi-document transactions. The anomalies are the same, and the tools available to prevent them differ. [CAP theorem explained](/blog/cap-theorem-explained-2026) covers the tradeoffs behind those choices.\n\n## Where to go next\n\nFor the transaction guarantees isolation sits inside, read [ACID transactions explained](/blog/acid-transactions-explained-2026). For the concurrency-control mechanism underneath most of this, [MVCC explained](/blog/mvcc-explained-2026), and for the durability half of the picture, [write-ahead logging](/blog/write-ahead-logging-explained-2026).","hue":304,"metaTitle":"Database Isolation Levels and Concurrency Bugs","metaDescription":"What each SQL isolation level actually prevents, why Read Committed allows lost updates, and how to fix concurrency bugs without going fully serializable.","keywords":["database isolation levels","read committed vs repeatable read","serializable isolation","lost update problem","write skew","select for update","transaction concurrency bugs","sql isolation explained 2026"],"tldr":["**Your default is probably Read Committed** — which permits non-repeatable reads and write skew, and most application bugs under concurrency trace to that.","**Read Committed does not stop lost updates** — read-modify-write across two transactions silently discards one, and this is the most common real bug.","**Serializable is correct and costs retries** — you must handle serialization failures and retry, or you have swapped one bug for another.","**SELECT FOR UPDATE fixes most cases without raising the level** — an explicit row lock is cheaper and more targeted than global stricter isolation.","**Skip:** raising the isolation level globally to fix one endpoint. Target the transaction that needs it."],"readTime":"5 min read","wordCount":1029},"related":[{"slug":"acid-transactions-explained-2026","title":"ACID transactions explained in 2026: what the guarantees really mean","excerpt":"ACID is four guarantees that make database transactions reliable, but each property has nuance that matters in distributed systems. Here is the real story.","category":"code","date":"April 8, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":322},{"slug":"transactions-explained-2026","title":"Database transactions explained in 2026: ACID, isolation, and beyond","excerpt":"Transactions are the safety net of every database write. This 2026 guide covers ACID, isolation levels, deadlocks, and the patterns that prevent data corruption.","category":"code","date":"April 21, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":310},{"slug":"database-transactions-explained-2026","title":"Database transactions explained in 2026: ACID, isolation, and real examples","excerpt":"Transactions are the mechanism that keeps your database consistent when multiple operations run concurrently. This is the 2026 guide to ACID properties, isolation levels, and the patterns that prevent data corruption.","category":"code","date":"April 13, 2026","author":"ByteLedger Team","readTime":"6 min read","hue":318}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"crypto-tax-lots-2026","title":"Crypto Tax Lots: Why Your Basis Records Matter More Now","excerpt":"Which specific units you sold determines your gain, and reporting rules have tightened around how lots are tracked. Here is what to keep and why.","category":"finance","date":"August 12, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":292},"older":{"slug":"dead-letter-queues-explained-2026","title":"Dead Letter Queues: Where Failed Messages Should Go","excerpt":"A message that cannot be processed will retry forever unless you catch it. Here is how dead letter queues work, what to put in one, and the replay discipline.","category":"code","date":"August 12, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":278}}