The Runtime Theory
Distributed Systems

Two-Phase Commit and Why It Fails

How two-phase commit coordinates atomic transactions across machines, why a crashed coordinator blocks every participant, and why Paxos and Raft now carry the decision.

The Runtime Theory Team3 min read#two-phase-commit#transactions#consensus#distributed-systems
On this page

Two-phase commit (2PC) is the protocol that makes a transaction touching multiple machines behave atomically: either every participant applies it, or none do. It is the machinery behind XA transactions, distributed SQL, and every "distributed transaction" your ORM claims to support. It also has a structural failure mode — blocking — that no amount of tuning removes. Understanding 2PC means understanding both halves: the protocol, and the moment it cannot finish.

The prepare phase

A coordinator (transaction manager) drives the protocol. It sends a PREPARE message to every participant — each shard, database, or queue holding a piece of the transaction. Each participant acquires its locks, writes a "prepared" record to its durable write-ahead log, fsyncs it, and replies.

text
coordinator:
  send PREPARE to all participants
  wait for votes (with timeout T)
 
participant:
  lock local resources
  append "prepared(tx)" to WAL; fsync
  reply YES        # promise: can commit, and cannot abort alone
  # or reply NO    # cannot commit, coordinator must abort

The fsync before the YES is not ceremony. A YES is a promise that the participant will be able to commit this transaction even if it crashes in the next millisecond. That promise is only enforceable if the prepared state is on disk first. A participant that votes YES surrenders the right to abort; a participant that has not yet voted may still abort.

The commit phase

If every participant voted YES, the coordinator appends COMMIT to its own log, then sends COMMIT to each participant. Each participant applies the transaction, releases locks, and replies ACK. Only after all ACKs arrive does the coordinator forget the transaction. A single NO — or a vote lost to a timeout — sends ABORT to everyone.

text
coordinator:
  if all votes YES:
    append COMMIT to local log; fsync
    send COMMIT to all participants
    wait for ACKs
  else:
    send ABORT to all participants

Three rules keep 2PC correct, and all three are also its fragility:

  1. A participant that voted YES cannot change its mind.
  2. The coordinator must not send COMMIT before it has all the votes.
  3. A participant in the prepared state holds its locks while it waits for the decision.

Blocking: the failure 2PC cannot survive

The classic failure: the coordinator crashes after collecting YES votes but before sending COMMIT. Every participant that voted YES is now in doubt — it holds locks on a transaction whose outcome it cannot learn. If the coordinator's disk is gone or the machine never returns, that state is permanent: the participants are blocked forever.

No participant can resolve this alone. Aborting would violate the promise it made in prepare; committing could violate atomicity if other participants committed or aborted differently. The only safe action is to wait for a decision that may never arrive. This is the blocking problem, and it is structural: 2PC has exactly one source of decisions, and losing it loses the transaction.

Crash recovery only softens the blow. A recovered coordinator replays its log to find the decision — but only if the decision was logged durably before the crash. A network partition does the same damage: a coordinator that cannot reach a quorum of participants cannot safely decide, so it waits, and so do they.

Why Paxos and Raft replace it

Three-phase commit (3PC) adds a pre-commit state and reduces blocking, but reintroduces it when the network partitions. The real fix used in production is to remove the single point of decision: run the coordinator as a replicated state machine.

Spanner and CockroachDB do exactly this — Paxos or Raft replicates the coordinator's log and each participant's prepared state. The commit decision becomes a consensus decision: a new leader can recover the decision from the replicated log even when the original coordinator is gone, so participants never block on a lost decision. The participant can even ask a quorum "was this transaction committed?" and get a definitive answer.

The honest summary: 2PC is still the right mechanism for atomicity — the prepare/commit handshake, the durable promises, the lock discipline. Consensus is the substrate that makes the decision durable. 2PC without consensus is a protocol whose failure mode is "everything stops"; 2PC over consensus is what distributed databases actually run.

When to avoid it

2PC costs two round trips plus a minimum of three fsyncs per transaction, and it holds locks across machines for the whole window. For high-throughput systems the alternatives are usually better: sagas with compensating actions where atomicity is negotiable, single-writer-per-record designs that need no coordination at all, and idempotent writes where "apply twice" is cheaper than "apply atomically." Choose 2PC when atomicity across machines is non-negotiable — and then choose consensus underneath it, or accept that a coordinator crash can stop your system until a human intervenes.