Atomic commit with two-phase commit
Open in the interactive app: practice, quiz, mock interview →In one minute
- Two-phase commit makes a transaction that spans several resource managers (databases, queues, shards) either commit everywhere or nowhere.
- Phase 1 asks every participant to prepare (make the change durable and promise it can commit). Phase 2 tells all of them the single decision.
- It is atomic but blocking: a participant that voted yes cannot decide alone, so if the coordinator dies at the wrong moment, locks stay held until it returns.
- It works well inside one system that replicates its coordinator (Spanner, CockroachDB, sharded databases). Across independent services it is usually the wrong tool.
How it works
The roles
A coordinator (often the transaction manager in the application server, or one of the shards) drives the protocol. Participants each own a piece of the transaction's data. Every party has a durable log, and the protocol's correctness depends on writing to it at the right moments.
Phase 1: prepare
The coordinator sends PREPARE to every participant. Each participant checks it can commit (constraints, locks, disk), writes all the transaction's changes and a prepared record to its log, forces the log to disk, and replies YES. From this moment it has given up the right to abort on its own: it must keep the locks and wait. If anything is wrong, it replies NO and aborts locally. A participant that does not reply in time is treated as NO.
Phase 2: commit or abort
If every vote is YES, the coordinator writes a commit record to its own log and forces it. That write is the commit point. It then sends COMMIT to everyone. If any vote is NO, it decides abort. Participants apply the decision, release locks, and acknowledge. Once all acknowledge, the coordinator writes an end record and can forget the transaction.
Failure handling
- Participant crashes before voting: the coordinator times out and aborts.
- Participant crashes after voting
YES: on restart it finds the prepared record and must ask the coordinator for the outcome before releasing locks. - Coordinator crashes before the decision is logged: on recovery it finds no decision and aborts (presumed abort).
- Coordinator crashes after logging the decision: on recovery it re-sends the decision.
The bad case is the window between a participant voting YES and receiving the decision. If the coordinator is down then, the participant is in doubt. It cannot commit (another participant may have voted NO) and cannot abort (the coordinator may have logged commit). It blocks, holding row locks, until the coordinator recovers. Participants could ask each other, but if all of them voted YES and none heard the decision, they still cannot know.
Cost
A commit needs two network round trips plus forced log writes at every participant and at the coordinator. Gray and Lamport count the normal case precisely: with N resource managers, participants learn the outcome after four message delays and 3N - 1 messages, and two stable-storage write delays if everyone prepares in parallel (Gray and Lamport). Across availability zones that adds a few milliseconds; across regions, hundreds. Locks are held for the whole duration, so throughput on hot rows falls with latency. Participant count matters too: Google reports that 2PC in Spanner works well up to tens of participants, but abort rates and latency climb sharply with hundreds (F1, VLDB 2013).
Down the stack
Layer 1The contract
2PC promises atomicity and durability of the outcome: every participant ends with the same decision, and once the decision is logged it survives any crash. It does not promise progress: a participant in the prepared state may wait indefinitely for the coordinator. Isolation comes from each participant's own concurrency control, and prepared transactions keep their locks until the decision arrives, even across a restart of the participant.
Layer 2The mechanism
In PostgreSQL, PREPARE TRANSACTION 'gid' detaches the transaction from the session, writes a prepare record to the WAL, and keeps its locks; state for long-lived prepared transactions is moved to files under pg_twophase at checkpoint, so it survives a restart. COMMIT PREPARED 'gid' or ROLLBACK PREPARED 'gid' finishes it, possibly from another session, and pg_prepared_xacts lists what is pending. A transaction that touched temporary tables, held a WITH HOLD cursor or ran LISTEN/NOTIFY cannot be prepared, and the docs say plainly that the command is for transaction managers, not applications (Postgres docs). The feature is off by default (max_prepared_transactions = 0), and should stay off unless something will reliably close prepared transactions (Postgres docs). The X/Open XA interface used by Java transaction managers names the same steps xa_prepare, xa_commit, xa_rollback, and xa_recover, which returns in-doubt transaction ids after a crash. Two standard optimizations: a participant that only read data votes read-only and skips phase 2, and presumed abort lets the coordinator skip logging aborts, since "no record" already means abort. Spanner runs the same protocol, but each participant's prepare record and the coordinator's commit record are written through Paxos groups; Google's F1 team measured that cross-group 2PC usually doubles commit latency compared with a single-group commit (F1, VLDB 2013). Gray and Lamport show this is no accident: classic 2PC is the special case of Paxos Commit with zero tolerated coordinator faults (Gray and Lamport).
Layer 3The machine
The forced writes dominate. Each participant must fsync its prepare record before voting, and the coordinator must fsync its commit record before announcing it: two sequential durable writes on the critical path, each about 50 µs to 1 ms on SSDs and 5 to 10 ms on spinning disks, with group commit sharing one fsync across many transactions. The messages are four one-way trips per participant (prepare, vote, commit, acknowledge), so two round trips before locks are released. While a transaction is prepared, its locks sit in the shared lock table and its transaction id holds back the oldest visible snapshot, so vacuum cannot remove dead rows newer than it. A prepared transaction forgotten for days causes table bloat and, eventually, transaction id wraparound pressure.
Why it's built this way
- Each participant can fail independently, so no one can commit until everyone has promised that they can.
- A promise must survive a crash, so the prepare vote is forced to disk before it is sent.
- Someone must own the single outcome, so a coordinator decides and logs it durably before telling anyone.
- Once a participant has promised, it cannot know the outcome without the coordinator, so it must block.
- Blocking is unacceptable when the coordinator is a single machine, so production systems replicate the coordinator's log with consensus.
Diagram
sequenceDiagram
participant C as Coordinator
participant A as Participant A
participant B as Participant B
C->>A: PREPARE
C->>B: PREPARE
A->>A: force prepared record
B->>B: force prepared record
A-->>C: YES
B-->>C: YES
C->>C: force COMMIT record (commit point)
C->>A: COMMIT
C->>B: COMMIT
A-->>C: ACK
B-->>C: ACK
C->>C: write ENDWorked example
A sharded ledger moves money between accounts on different shards: debit shard 3, credit shard 8. Each shard is a Raft group of three replicas, so "forcing the log" means a majority write, about 2 ms in-region.
Latency per transfer: prepare round trip plus replicated log write on both shards (about 3 ms), the coordinator's replicated commit record (about 2 ms), then the commit message (about 1 ms before the client can be answered, if the coordinator replies once the decision is durable). Roughly 6 ms, against 2 ms for a single-shard transaction. At 5,000 cross-shard transfers per second, locks on each account are held for about 6 ms, so one hot account caps out near 160 transfers per second.
Because the coordinator's log is itself replicated by Raft, a coordinator node crash does not block: another replica takes over and finishes the protocol. This is how Spanner and CockroachDB make 2PC practical. CockroachDB goes one step further with parallel commits: the transaction record is written as STAGING in parallel with the data, the transaction counts as committed once every listed write has reached consensus, and the client waits for one consensus round instead of two, so this 6 ms transfer would lose one 2 ms consensus write and land near 4 ms (Cockroach Labs). In contrast, an XA transaction across a Postgres database and a message broker, coordinated by one application server, blocks both if that server dies after prepare, and an operator may have to resolve in-doubt transactions by hand.
In the wild
- Cockroach Labs, 2019: halving the cost of 2PC over consensus. CockroachDB's commit was 2PC on Raft: write intents, then mark the transaction record committed, two sequential consensus rounds. Parallel commits, shipped on by default in 19.2, writes a
STAGINGrecord listing the in-flight writes alongside them, so a transaction is committed as soon as all of them are replicated. Cross-range commit latency fell by about half, and if the coordinator dies, any reader can run status recovery to finish or abort the transaction (Cockroach Labs). - Jepsen and YugabyteDB, 2019: serializable cycles when the masters were unhealthy. YugabyteDB's Spanner-inspired transactions create a status record, write provisional records, then mark the status committed. Jepsen found dozens of G2-item anomalies (anti-dependency cycles) on plain primary-key reads and updates, tied to master crashes, restarts or pauses. The fix shipped in 1.3.1.2-b1, and both parties advised against relying on serializable SQL for safety-critical work at that stage (Jepsen).
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| 2PC with single coordinator | Atomic, simple model | Blocks on coordinator failure | All participants trusted, short outages tolerable |
| 2PC over replicated coordinator | Atomic and non-blocking in practice | Needs consensus underneath | Distributed databases, sharded stores |
| Saga | No cross-service locks, high availability | Only eventual consistency, compensations | Long-running, cross-service workflows |
| Outbox plus events | Simple, reliable local commit | Asynchronous, consumers must be idempotent | One service's change must reach others |
| Avoid distribution | No coordination at all | May constrain data model | Data that changes together can live together |
Pitfalls in production
- Orphaned prepared transactions. A crashed coordinator leaves prepared transactions holding locks and blocking vacuum. Monitor prepared transactions by age and have a runbook to resolve them.
- Timeouts that unilaterally commit or abort. A participant "fixing" an in-doubt transaction by itself breaks atomicity. Only the coordinator's log decides.
- Heterogeneous XA. Drivers and brokers implement recovery inconsistently, and some cannot join at all: Kafka's proposal to act as a 2PC participant (KIP-939) is accepted but still unreleased (KAFKA-15370). Test crash recovery for every resource you enlist, or avoid XA.
- Latency stacking. Adding a cross-region participant turns a 5 ms commit into 200 ms and collapses lock throughput. Keep participants co-located.
- Using 2PC between microservices. It couples availability: any service down blocks all. Prefer sagas or the outbox pattern.
Leadership lens
- Ask whether the data really needs atomic multi-party commit, or whether it can be co-located so a local transaction suffices.
- If 2PC is needed, ask how the coordinator is made highly available and how in-doubt transactions are found and resolved.
- Put in-doubt transaction counts and age on dashboards and in on-call runbooks.
- Remember the organisational cost: 2PC across team boundaries means every team's outage becomes everyone's outage.
Check yourself
Why can't a participant that voted
YESsimply abort after a long timeout?Show answer
The coordinator may already have logged commit and told others to commit, so aborting would make the outcome differ across participants.
What exactly is the commit point in 2PC?
Show answer
The moment the coordinator's commit record is durable in its log; after that, the transaction is committed even if no participant has heard yet.
Why does running the coordinator on a consensus group reduce blocking?
Show answer
The decision is replicated, so if the coordinator node fails, another replica holds the log and can finish the protocol.
Sources and further study
Primary
- Skeen, Nonblocking Commit Protocols (SIGMOD 1981): proves why 2PC's prepared state must block and what a non-blocking protocol needs.
- Shute et al., F1 (VLDB 2013): 2PC over Paxos in production, with measured commit latencies.
- Corbett et al., Spanner (OSDI 2012): the replicated-coordinator design most distributed SQL systems copy.
Systems docs
- PostgreSQL: max_prepared_transactions: why the participant feature is disabled by default.
- Parallel Commits (Cockroach Labs): the
STAGINGrecord and implicit commit. - Jepsen: YugaByte DB 1.3.1: what a distributed transaction protocol looks like under fault injection.
Books
- Designing Data-Intensive Applications (1st edition), Martin Kleppmann, chapter 9 "Consistency and Consensus", section "Distributed Transactions and Consensus": 2PC, coordinator failure and XA in practice.
- Database Internals, Alex Petrov, chapter 13 "Distributed Transactions": the 2PC state machine, its failure cases and Spanner's variant.
Other curricula
- MIT 6.5840 lecture notes: Distributed Transactions: teaches 2PC together with two-phase locking as the two halves of a distributed transaction.
- Kleppmann, Cambridge Concurrent and Distributed Systems notes: introduces 2PC and then fault-tolerant 2PC built on total order broadcast.
Watch
- Distributed Systems 7.1: Two-phase commit (Martin Kleppmann): a short Cambridge lecture on atomic commitment and 2PC.
- Lecture 12: Distributed Transactions (MIT 6.824): Robert Morris on concurrency control and two-phase commit.
Free resources
- Wikipedia: Two-phase commit protocol: the protocol, its logging, and the blocking problem.
- PostgreSQL: PREPARE TRANSACTION: a real participant-side implementation and its operational caveats.
- Martin Fowler: Two-Phase Commit: the pattern with failure cases worked through.
- Gray and Lamport: Consensus on Transaction Commit: why 2PC blocks and how Paxos Commit removes it.
- The Paper Trail: Two-Phase Commit: a clear walk through the protocol and its failure modes.