Engineering Atlas › System Design Fundamentals

Atomic commit with two-phase commit

12 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

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

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

  1. Each participant can fail independently, so no one can commit until everyone has promised that they can.
  2. A promise must survive a crash, so the prepare vote is forced to disk before it is sent.
  3. Someone must own the single outcome, so a coordinator decides and logs it durably before telling anyone.
  4. Once a participant has promised, it cannot know the outcome without the coordinator, so it must block.
  5. 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 END

Worked 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

Trade-offs

OptionStrengthsWeaknessesChoose when
2PC with single coordinatorAtomic, simple modelBlocks on coordinator failureAll participants trusted, short outages tolerable
2PC over replicated coordinatorAtomic and non-blocking in practiceNeeds consensus underneathDistributed databases, sharded stores
SagaNo cross-service locks, high availabilityOnly eventual consistency, compensationsLong-running, cross-service workflows
Outbox plus eventsSimple, reliable local commitAsynchronous, consumers must be idempotentOne service's change must reach others
Avoid distributionNo coordination at allMay constrain data modelData that changes together can live together

Pitfalls in production

Leadership lens

Check yourself

  1. Why can't a participant that voted YES simply 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.

  2. 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.

  3. 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

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →