Skip to main content
Database Isolation Levelslesson 4 of 4 · 2 min read

Serializable, and When to Pay for It

As if they ran one at a time

Serializable is the top rung. The database guarantees the outcome of your concurrent transactions matches some serial ordering of them, so every anomaly on the list becomes impossible, write skew and phantoms included.

Your reasoning collapses to one question: would this be correct if transactions ran one at a time? That is the only question most people can answer reliably.

What it costs, and where

How your database implements it matters, because the price depends on that. Traditional locking takes range locks on everything you read and holds them until commit. That is correct, and it makes readers and writers block each other until the rate of work sinks.

Postgres takes a different route. Transactions run optimistically on snapshots while the engine watches the dependencies between them, and it aborts one whenever a pattern appears that could produce a non-serializable result. Nothing blocks, and transactions fail with a serialization error your application must retry.

That overhead is modest when contention is low and brutal on a hot row everybody fights over. CockroachDB runs serializable as its only level, which tells you the cost is payable when the engine is designed around it.

Spend it deliberately, because levels are set per transaction and not per database. Keep Read Committed as the default. Apply Serializable to the handful of transactions holding invariants that span rows: balancing a ledger, seat inventory, the on-call rule from the previous lesson, and uniqueness rules too complex for an index.

Wrap them in a retry helper with a capped number of attempts and a little randomness, keep them short so the conflict window stays small, and watch the abort rate. A climbing count of serialization failures is your early warning that contention is growing.

Worked example

A brokerage app enforces that a customer's positions never exceed their margin limit, a rule spanning the positions and collateral tables. At Repeatable Read, two simultaneous orders from customer Chen each check the limit against the same snapshot, each pass individually, and together push him 8,000 dollars over. Compliance notices within a week. The team moves order placement, and only order placement, to Serializable. Under load testing at 400 orders per second, about 1.5 percent of transactions abort with serialization errors; a retry wrapper with three attempts and 10 ms jittered backoff absorbs nearly all, adding under 15 ms at p99, the figure 99 requests in every 100 come in under. Everything else in the app stays at Read Committed and unaffected. Chen's racing orders now serialize: the second one recomputes against the first one's committed result and is correctly rejected.

Database Isolation Levels: wrapping up

In the real world

  • 01PostgreSQL defaults to Read Committed and implements true Serializable via Serializable Snapshot Isolation (since 9.1), which aborts conflicting transactions with error 40001 instead of blocking them.
  • 02MySQL InnoDB defaults to Repeatable Read and uses next-key locks to block phantoms in locking reads, one reason the same schema behaves differently after a port to Postgres.
  • 03CockroachDB made serializable its standard isolation level, arguing that anomaly-driven bugs cost more than the retry overhead, and requires clients to handle transaction retries.
  • 04Amazon found in production incident reviews that snapshot isolation write skew caused real bugs, and DynamoDB transactions sidestep the issue with optimistic concurrency checks on every item touched.
  • 05Oracle famously labels snapshot isolation as its Serializable level, a naming choice that lets write skew occur at a level called Serializable and keeps database consultants employed.

Questions people ask

Which isolation level should I actually use?

Start with your database's default, Read Committed on Postgres, and write hot-path updates as single atomic SQL statements so lost updates cannot happen. Reserve Serializable for the few transactions enforcing invariants across multiple rows or tables, and give those a retry loop. Raising the global level everywhere buys anomalies you don't have at a throughput cost you will notice.

Why did my transaction fail with a serialization error when nothing seemed wrong?

At Repeatable Read or Serializable, the database aborts transactions whose reads and writes overlap dangerously with concurrent ones, even if your specific run would have been fine. It is the engine being conservative to guarantee correctness. The fix is not lowering the level but retrying the transaction, ideally automatically with a capped backoff.

What is write skew and why doesn't Repeatable Read stop it?

Write skew is two transactions reading overlapping data, then each writing different rows, so that both commit but the combined result breaks an invariant, like two doctors both leaving call because each saw the other on duty. Repeatable Read only guarantees each transaction a stable snapshot; since neither wrote a row the other read, no conflict is detected. Only Serializable tracks those cross-transaction dependencies.

Quick review

Read Uncommitted:
can read uncommitted (in-flight) writes. Dirty reads possible. Almost never used
Read Committed:
only see committed data. Prevents dirty reads. PostgreSQL default
Repeatable Read:
rows read at transaction start stay stable. Prevents non-repeatable reads. MySQL InnoDB default
Serializable:
full isolation. Transactions appear sequential. Prevents phantom reads. Heaviest locking
Dirty read → Non-repeatable read → Phantom read:
each level closes one more anomaly
Most apps use Read Committed or Repeatable Read. Use Serializable only for financial/critical operations
the trade-off

Higher isolation = fewer anomalies but more lock contention and lower throughput.

in the room

Payment systems: Serializable. User profiles: Read Committed is usually enough. Know what anomalies each level prevents.