PostgreSQL Serializable isolation aims for committed transactions to have an outcome consistent with some serial execution order. Applications can still run concurrently, and PostgreSQL may reject a transaction with a serialization failure when it cannot safely accept the combined outcome. The application must be prepared to retry correctly.
The important unit is the complete decision and transaction, not only the final statement that failed. This guide explains retry boundaries, side effects, and testing so a stronger isolation setting supports the business invariant rather than creating an unhandled error path.
Define the invariant being protected
State the rule that depends on several reads or writes. Examples include maintaining an allowed allocation or checking a condition before creating related records. Identify which data informs the decision and which updates establish the result.
Review whether a constraint or explicit locking strategy better expresses part of the invariant. Serializable isolation does not replace uniqueness, foreign keys, authorization, or a well-designed schema. These mechanisms answer different correctness questions.
Keep every relevant transaction path in the design. If one writer uses a different assumption or bypasses required logic, setting isolation on another path may not produce the business guarantee the team expects.
Understand serial behavior without serial execution
Serializable does not mean the database literally runs one transaction at a time. It provides a documented isolation guarantee for accepted transactions while detecting certain conflicts. Concurrent attempts can therefore fail and require another complete attempt.
Do not interpret a serialization failure as evidence that PostgreSQL is broken. It can be the expected way the system protects a valid outcome. The application should recognize the relevant structured error and follow a bounded policy.
Read the installed version’s isolation documentation. PostgreSQL’s levels and behavior have specific semantics, and assumptions borrowed from another database engine may be wrong. Test through the actual driver and deployment configuration.
Set the boundary before the decision
Choose the isolation level before the transaction performs the reads that inform its decision. The application should not conduct a critical lookup outside the protected transaction and then assume a later serializable write validates that earlier result.
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- Perform the reviewed decision and its database changes here.
COMMIT;
This illustrates a boundary, not a complete business workflow. Use parameterized queries and the real application’s authorization checks inside the approved design. The placeholder comment does not enforce an invariant by itself.
Keep the transaction short enough for operational needs. Avoid waiting for user confirmation or slow unrelated external services while holding the decision open. Strong isolation is not permission to accumulate long-lived work.
Retry all relevant reads and decisions
After a serialization failure, retry the complete transaction from a fresh valid state according to the driver and PostgreSQL guidance. Reusing an old decision can repeat the same invalid assumption even if the final SQL statement is issued again.
Include the logic that chooses values and statements within the retry boundary where required. A cached availability result or previously calculated allocation can be stale. The application must re-evaluate the business condition, not only replay a stored update.
Reset the failed transaction and client state through supported rollback and connection handling. Do not return an unusable transaction to a pool or continue issuing commands as if the exception had no effect.
Bound retries and observe contention
Set maximum attempts and an overall time budget. Backoff or jitter can reduce immediate repeated conflicts where appropriate, but neither provides an unlimited success guarantee. Persistent contention needs investigation.
Keep the final failure result honest and usable. The caller may need to retry later, narrow the operation, or receive a controlled conflict response. Do not claim success because the retry loop ended.
Measure serialization failures by operation and relevant bounded categories. Investigate hot keys, unnecessarily large transactions, and workload patterns. Raising the retry count indefinitely can hide a capacity or design problem while increasing latency.
Keep external actions outside accidental replay
A transaction retry can repeat application code. If that code sends messages, charges an external service, or publishes content, the side effect can occur multiple times. Database rollback does not reverse those actions automatically.
Use a durable workflow such as an appropriate outbox or supported idempotency mechanism when coordinating external effects. Define when the action becomes authorized and how uncertain outcomes are reconciled. The retry policy must fit those semantics.
Avoid treating a lost commit response as identical to a confirmed serialization failure. The database outcome may be uncertain after a transport failure. Reconcile state using the operation’s identity rather than blindly assuming nothing committed.
Test real concurrent decisions
Create a controlled test where two transactions make decisions from overlapping data and attempt changes. Verify that the final state satisfies the invariant and that rejected attempts follow the whole-transaction retry policy.
Assert the application re-reads the relevant state. A test that merely counts another call to the last update can miss a stale decision. Include attempts exhausted under a bounded policy and inspect the returned outcome.
Test through the actual pool and driver, including structured error classification and rollback behavior. A hand-written console demonstration does not establish that the production retry wrapper owns the correct scope.
A practical allocation workflow
Suppose two requests attempt to allocate the last available units based on a shared aggregate. Each serializable transaction reads the required state, checks the approved rule, and records its allocation. A serialization failure causes the complete decision to run again with current evidence.
The application records a durable operation identity and publishes a notification through its approved post-commit workflow. If all retry attempts fail, it returns a clear conflict or temporary failure instead of sending a success message for uncommitted work.
If contention becomes common, review the data model and transaction size. A stronger isolation setting is part of correctness, not a substitute for a scalable workflow.
Document the complete contract
Record the invariant, isolation scope, retryable error policy, maximum budget, external effects, and final failure behavior. Review this contract when business logic or the driver changes.
Keep diagnostics free of credentials and unnecessary record contents. Useful evidence describes the operation and retry outcome without exposing the private decision inputs to every monitoring viewer.
Frequently asked questions
Does Serializable prevent every application error?
No. It provides a database isolation guarantee, not authorization, input validation, or correctness of the business rule itself.
Can I retry only the statement that failed?
Not as a general serialization-failure strategy. Retry the complete relevant transaction and decision according to PostgreSQL guidance.
Where are the guarantees documented?
Read the PostgreSQL transaction isolation reference. For another class of transaction conflict, see our deadlocks guide.