A PostgreSQL transaction at SERIALIZABLE isolation can read a set of rows while a concurrent transaction writes data relevant to that read without the reader taking a blocking row lock. PostgreSQL preserves serializable outcomes by tracking read-write dependencies and rejecting a transaction when the observed dependency structure could admit a serialization anomaly.
That mechanism differs from treating every read predicate as a barrier against matching writes. The database keeps MVCC snapshot behavior, adds SIReadLock state for dependency detection, and makes transaction retry part of the isolation contract.
SIRead state records a dependency boundary
PostgreSQL calls the tracking mechanism predicate locking, but an SIReadLock is not a conventional lock that makes a writer wait. Its purpose is to record that a serializable transaction read data whose later modification by an overlapping transaction can create a read-write dependency.
The distinction matters operationally. A query such as:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT account_id
FROM account
WHERE balance < 0;can establish SIRead state for data accessed by the query. Another transaction is not prohibited merely because it writes a row covered by that state. PostgreSQL can allow the write and retain enough information to reason about the dependency between the transactions.
As a result, seeing SIReadLock entries in pg_locks does not imply that matching writers are queued behind those entries. Ordinary lock conflicts and serializable dependency tracking are separate mechanisms.
Snapshot stability alone does not provide serial execution
PostgreSQL REPEATABLE READ already gives a transaction a stable snapshot. Successive reads do not begin seeing rows committed by concurrent transactions after that snapshot is established. A stable snapshot, however, can still permit a group of transactions to make decisions from states that cannot all fit into one serial order.
Consider two on-call rows:
CREATE TABLE duty (
engineer_id bigint PRIMARY KEY,
active boolean NOT NULL
);Assume two rows are active and an application rule requires at least one to remain active. Two concurrent transactions can each read both rows, observe two active engineers, then deactivate a different row.
Under snapshot isolation, each transaction can base its write on a snapshot that excludes the other’s write. The final state can violate the application rule even though neither transaction observed a state with zero active rows.
At SERIALIZABLE, PostgreSQL tracks the relevant read-write dependencies. If allowing all participating transactions to commit would create an unsafe dependency pattern, at least one transaction is rejected with a serialization failure. The application must retry the full transaction rather than treating every statement that executed successfully as durable.
Query plans affect tracking granularity
Predicate tracking is tied to data actually accessed, so the execution plan influences the SIRead state PostgreSQL records. Tracking can occur at tuple, page, or relation granularity, depending on the access path and internal promotion of finer-grained state.
A sequential scan requires relation-level predicate tracking. An index-backed access path can permit finer-grained tracking when the index access method supports it. PostgreSQL can also combine numerous fine-grained entries into coarser entries to stay within predicate-lock memory limits.
This granularity does not alter the serializability guarantee. It can alter which concurrent writes appear relevant to the dependency detector. Coarser tracking can therefore increase serialization failures that would not arise with more precise tracking, while still preserving correctness.
The plan is consequently part of the observable concurrency footprint. Two semantically equivalent queries can impose different predicate-tracking granularity if their plans access data differently.
Serialization failure is distinct from deadlock recovery
A deadlock arises from a cycle of blocking lock waits. PostgreSQL detects such a cycle and aborts one participant because none can progress otherwise.
SIRead locks do not block writers, so they do not themselves form blocking deadlock edges. Serializable Snapshot Isolation instead observes read-write dependencies among concurrent transactions and can abort a transaction to prevent a non-serializable result.
Both cases can surface as transaction aborts, but the failure mechanisms are different. Changing statement order can break a conventional lock deadlock by changing lock acquisition order. The same change does not generally remove a serialization conflict when the transactions still make incompatible decisions from overlapping snapshots.
Applications using SERIALIZABLE therefore need retry handling even when conventional deadlocks have been eliminated.
Read-only work can still participate in dependency analysis
A transaction does not need to write rows to matter to serializable dependency analysis. A read-only serializable transaction can observe a snapshot whose relationship with concurrent read-write transactions requires tracking until PostgreSQL can establish that no serialization anomaly can result.
For workloads that require a read-only snapshot safe from later serialization failure, PostgreSQL provides SERIALIZABLE READ ONLY DEFERRABLE. Such a transaction can wait before executing queries until PostgreSQL obtains a snapshot considered safe for this purpose. Once that safe snapshot is acquired, the read-only transaction avoids the later serialization-failure risk associated with an ordinary serializable read-only transaction.
The trade changes location rather than removing coordination: waiting can occur before useful work begins instead of a serialization failure surfacing after reads have already run.
Tracking state can outlive the transaction that created it
Conventional transaction locks are often discussed in terms of release at commit or rollback. SIRead state has a different lifetime requirement because PostgreSQL may still need it while overlapping transactions remain active.
An SIRead lock can therefore remain visible after its originating transaction commits. Retaining that state allows dependency analysis to include transactions whose execution intervals overlap even when their commit boundaries differ.
This property also means that a large population of long-running serializable transactions can expand the interval over which dependency information must remain relevant. PostgreSQL manages predicate-lock state with dedicated configuration limits and can promote fine-grained locks to coarser levels when necessary.
Serializable semantics include the possibility of retry
A successful serializable commit carries a stronger property than a stable snapshot: the committed result is consistent with some serial execution of the successfully committed serializable transactions involved. PostgreSQL reaches that property without forcing every read-write interaction into a blocking lock wait.
The cost boundary is explicit. A transaction can execute statements, perform reads, and reach late execution stages before PostgreSQL reports that it cannot safely commit in the current dependency structure. Code that publishes irreversible external effects before commit cannot assume statement success predicts transaction success.
For database-contained work, retrying the complete transaction gives PostgreSQL a new concurrency schedule and snapshot from which the operation can proceed. For work coupled to external systems, the transaction boundary must be coordinated with those effects using an appropriate delivery or idempotency design.
SERIALIZABLE is therefore not merely a stronger snapshot mode. In PostgreSQL it adds dependency observation around snapshot execution, permits non-blocking read-write overlap in many cases, and converts unsafe concurrency patterns into explicit transaction failure rather than committed serialization anomalies.