Hello! Welcome back.
In our last lesson, we examined the underlying mechanisms of ACID properties in PostgreSQL, focusing on the performance trade-offs of Durability (via WAL and checkpoints) and Isolation (via MVCC, bloat, and HOT updates). We established that PostgreSQL's MVCC architecture is key to its high concurrency but introduces costs like write amplification and the need for vacuuming.
Today, we build directly on that foundation. Instead of the how of isolation, we'll explore the what—the specific guarantees offered. Our learning outcome is to configure and compare PostgreSQL isolation levels: Read Committed, Repeatable Read, and Serializable. We will analyze the concurrency phenomena, or "anomalies," that each level prevents and the performance price for that protection. This is critical for architecting correct and performant high-load systems, where choosing the right trade-off between consistency and throughput is paramount.
1. The Spectrum of Isolation and Concurrency Anomalies
Transaction isolation isn't a binary switch; it's a spectrum. At one end, you have maximum performance with weak guarantees, and at the other, you have maximum safety (serializability) with a higher performance cost due to increased contention or transaction rollbacks.
The SQL standard defines isolation levels by the anomalies they prevent. An anomaly is an outcome that would not be possible if the transactions had run one after another in some serial order. Let's define the most common ones.
System Design: Concurrency Control in Distributed System | Optimistic & Pessimistic Concurrency Lock
This video from the 'Concept && Coding' channel provides a detailed technical breakdown of the classic concurrency anomalies. While it discusses locking strategies more broadly, the explanations of the anomalies themselves are very clear and will set the stage for our discussion of how PostgreSQL's MVCC-based approach handles them.
Please watch the section 'Introduction to Isolation Levels and Concurrency Anomalies' from 15:41 to 27:15. Focus on understanding the definitions and examples for: Dirty Read: Reading uncommitted data from another transaction. Non-Repeatable Read: Reading the same row twice within a transaction and getting different values. Phantom Read: Running the same query twice within a transaction and getting a different set of rows.
To summarize and add a few more critical anomalies that we'll encounter:
- Dirty Read: Transaction T1 reads data modified by a concurrent transaction T2 that has not yet committed. If T2 rolls back, T1 has read data that never "existed."
- Non-Repeatable Read: T1 reads a row. T2 then modifies or deletes that row and commits. If T1 re-reads the row, it sees a different value or that the row is gone.
- Phantom Read: T1 runs a query with a
WHEREclause. T2 then inserts a new row that satisfies theWHEREclause and commits. If T1 re-runs its query, it sees a new "phantom" row. - Lost Update: T1 and T2 both read a value, say
X=10. T1 calculatesX+1and writesX=11. T2 also calculatesX+1and writesX=11. The first update is "lost" because the final value should have been12. - Write Skew: A generalization of the lost update. T1 reads some data
Aand makes a decision to write toB. Concurrently, T2 readsBand makes a decision to write toA. Both transactions' decisions are valid at the time they read the data, but the combined result violates a business invariant. For example, two on-call doctors check if at least one other doctor is on-call, see that there is, and both decide to take the day off.
Now, let's see how PostgreSQL's isolation levels handle these situations.
2. PostgreSQL Isolation Levels in Practice
PostgreSQL provides three of the four standard isolation levels. We will use a fantastic, in-depth guide to walk through them. It uses a practical booking system example with two concurrent users, Alice and Bob, to demonstrate the behavior of each level.
A Practical Guide to Taming Postgres Isolation Anomalies
We will be using 'A Practical Guide to Taming Postgres Isolation Anomalies' by Dan Svetlov as our primary reference. It provides excellent, hands-on SQL examples for each anomaly and isolation level.
We will refer to different sections of this article throughout the lesson. For now, you can open it and get a feel for the structure.
2.1. Read Committed: The Default
This is the default isolation level in PostgreSQL. Its behavior stems directly from the MVCC mechanism we discussed last time: each statement in a transaction gets a new snapshot of the database. This means a statement sees only data that was committed before it began.
A Practical Guide to Taming Postgres Isolation Anomalies
Let's explore what this means in practice. Please read the 'Read Committed' section of the guide.
Read the section titled 'Read Committed'. Pay close attention to the examples for Lost Update and Write Skew. Also, note the various mitigation techniques discussed, such as conditional updates (UPDATE ... WHERE), explicit locking (SELECT FOR UPDATE), and optimistic locking.
Key Characteristics of Read Committed:
- Anomalies Prevented:
- Dirty Reads: PostgreSQL's implementation of
Read Committedis stronger than the SQL standard's minimum requirement. Because a statement's snapshot only includes committed data, dirty reads are impossible.
- Dirty Reads: PostgreSQL's implementation of
- Anomalies Allowed:
- Non-Repeatable Reads & Phantom Reads: Since each statement gets a new snapshot, if a concurrent transaction commits between two of your
SELECTstatements, the secondSELECTwill see the new data. - Lost Updates & Write Skew: These are very common problems in
Read Committed. The classic "read-modify-write" cycle is unsafe without explicit protection, as the data you read can become stale by the time you write.
- Non-Repeatable Reads & Phantom Reads: Since each statement gets a new snapshot, if a concurrent transaction commits between two of your
- Performance: This level offers the highest concurrency with the least blocking, as readers don't block writers and writers only block other writers trying to modify the exact same rows.
- Configuration: You can set it explicitly with
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;, but it's the default.
Practical Implications:Read Committed is fast, but it places the burden of ensuring correctness on you, the developer. For any read-modify-write logic, you must use strategies like:
- Atomic
UPDATEstatements:UPDATE events SET available_seats = available_seats - 1 WHERE .... This is safe because the row-level lock taken by theUPDATEensures that the calculation on the right-hand side is based on the most up-to-date row version. - Pessimistic Locking (
SELECT ... FOR UPDATE): This explicitly locks the rows you read, preventing any other transaction from modifying them until your transaction commits. This is a powerful tool but reduces concurrency, as other sessions will block. It's a common pattern in payment processing or order matching, where you must serialize access to a specific resource. - Optimistic Locking: You add a version column to your table. The
UPDATEstatement then includesWHERE id = ? AND version = ?. If the row was modified by another transaction, its version will have changed, and yourUPDATEwill affect zero rows, signaling a conflict that the application must handle (usually by retrying).
2.2. Repeatable Read: A Stable View
The Repeatable Read level addresses the main weakness of Read Committed. It provides a more stable view of the database by using a single snapshot for the entire transaction, taken at the start of the first statement.
A Practical Guide to Taming Postgres Isolation Anomalies
Now let's see how upgrading the isolation level changes the behavior. Please read the 'Repeatable Read' section of the guide.
Read the section titled 'Repeatable Read'. Notice how Lost Updates and Write Skews now result in a serialization failure. Crucially, understand the 'Write Skew With Disjoint Write Sets' example, which is the key anomaly this level fails to prevent.
Key Characteristics of Repeatable Read:
- Anomalies Prevented:
- Non-Repeatable Reads & Phantom Reads: Because the entire transaction uses a single, fixed snapshot, you are guaranteed to see the exact same data for the life of the transaction. Concurrent commits are invisible.
- Lost Updates & "Simple" Write Skew: If your transaction tries to
UPDATEa row that has been modified by a concurrent transaction (which committed after your snapshot was taken), PostgreSQL detects this conflict. It doesn't block; instead, it aborts your transaction with aserialization failure.
- Anomalies Allowed:
- Write Skew with Disjoint Write Sets: This is the crucial limitation. If two concurrent
Repeatable Readtransactions read the same data but write to different rows, the database may not detect a conflict, leading to an anomaly. The article's example of Alice and Bob updating their own separate booking rows based on a sharedSUMis the canonical illustration of this.
- Write Skew with Disjoint Write Sets: This is the crucial limitation. If two concurrent
- Performance: It has slightly more overhead than
Read Committedand can significantly reduce throughput if conflicts are common, as failed transactions must be retried by the application. - Configuration:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Practical Implications:
This level is a significant step up in safety. It protects you from most common read-modify-write hazards. However, it introduces a new requirement for your application: you must be prepared to catch serialization failures and retry the entire transaction. This is a fundamental shift from the "wait for a lock" model of Read Committed.
2.3. Serializable: The Strongest Guarantee
This is the highest isolation level. It guarantees that the result of running transactions concurrently is the same as if they had been run in some serial order.

A Practical Guide to Taming Postgres Isolation Anomalies
Finally, let's examine the 'Serializable' level. Please read the corresponding section in the guide.
Read the section titled 'Serializable'. Focus on how it uses 'predicate locks' to prevent the 'Write Skew With Disjoint Write Sets' that Repeatable Read allowed. Also, note the rule that all interacting transactions must use this level for the guarantee to hold.
Key Characteristics of Serializable:
- Anomalies Prevented: All of them, including Write Skew with Disjoint Write Sets.
- How it Works: It does everything
Repeatable Readdoes, but adds more sophisticated conflict detection. It tracks not just the rows that were read, but the predicates (theWHEREclauses) used to read them. If a concurrent transaction commits a write that would have changed the result of a read in your transaction, it detects a dependency and fails one of the transactions. - Performance: This level has the highest overhead. It requires the database to track more dependency information and can result in more serialization failures than
Repeatable Read. - Configuration:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Practical Implications:Serializable provides the strongest correctness guarantee, simplifying application logic as you no longer need to reason about complex interleavings. However, it's not a silver bullet. The performance cost can be significant in high-contention workloads. Like Repeatable Read, it requires a robust application-level retry mechanism. It is best used for complex, multi-step business transactions where manual locking would be even more complex and error-prone, and where correctness is non-negotiable.
3. Comparison and Summary
The following table provides a clear visual summary of which anomalies are possible at each of PostgreSQL's isolation levels.

| Isolation Level | Dirty Read | Non-Repeatable Read | Phantom Read | Lost Update / Write Skew | Retry Logic Required? |
|---|---|---|---|---|---|
| Read Committed | No | Yes | Yes | Yes (without manual locking) | No (transactions block) |
| Repeatable Read | No | No | No | Yes (with disjoint sets) | Yes |
| Serializable | No | No | No | No | Yes |
Choosing the right level is a critical architectural decision:
- Use Read Committed (the default) for most simple queries and transactions. When you have a read-modify-write cycle, use explicit locking (
SELECT FOR UPDATE) for a pessimistic, blocking strategy, or optimistic locking for a non-blocking one. - Use Repeatable Read when you need a stable view of the data for the duration of a transaction (e.g., for a multi-statement report) and want protection against most write skews, and you are prepared to handle transaction retries.
- Use Serializable for complex transactions with invariants that span multiple tables/rows, where correctness is paramount and the logic of manual locking would be too difficult to prove correct. Ensure your application has a solid retry wrapper for these transactions.
4. Conclusion
Today we've moved from the mechanics of isolation to the guarantees they provide. We've seen that PostgreSQL offers a powerful set of tools to manage the trade-off between concurrency and correctness.
Key Takeaways:
Read Committedis fast and scalable but requires you to manually manage concurrency control for complex operations using techniques likeSELECT FOR UPDATE.Repeatable Readprovides a stable view of the data (snapshot isolation) and prevents many common anomalies, but it introduces the requirement for application-level transaction retries upon serialization failure. It is still vulnerable to write skews on disjoint data.Serializableoffers the strongest mathematical guarantee of correctness, preventing even the most subtle anomalies, at the cost of higher overhead and a greater likelihood of serialization failures that must be retried.
Preview of the Next Lesson:
We've covered the theory and seen examples. In the next lesson, "Demonstrate concurrency anomalies in PostgreSQL and resolve them using appropriate isolation levels," you will get a chance to apply this knowledge. We will set up a practical environment where you can trigger these anomalies yourself and then use the techniques we've discussed today to prevent them.