Hotshard
Distributed transaction

Three-Phase Commit

2PC freezes if the coordinator dies mid-commit. Does adding a phase really fix it?

Two-phase commit has one famous flaw: if the coordinator crashes at the wrong instant, every database that already voted yes is stuck β€” holding its locks, unable to commit or abort on its own, waiting for a coordinator that may never come back. Three-phase commit (3PC) is the classic attempt to fix exactly that. It slips one extra round of messages β€” a pre-commit phase β€” between the vote and the commit, so that a stranded participant can look at its own state and safely work out the answer instead of freezing. On paper it works: under the right failure model, 3PC never blocks. But there is an honest catch that keeps it out of almost every real system, and this page is about both halves β€” how the extra phase removes the block, and why a network partition brings it right back. If you haven't met the base protocol yet, read Two-Phase Commit first (/twopc); everything here assumes it.

~16 min read

The problem: 2PC blocks when the coordinator dies#

TL;DRthe 30-second version
  • 2PC blocks: after a participant votes yes it is 'in-doubt' β€” it has promised to commit but doesn't know the decision. If the coordinator crashes right then, the participant can't commit (maybe someone voted no) and can't abort (maybe the coordinator already logged commit). It waits, holding locks. See Two-Phase Commit (/twopc).
  • 3PC's idea: add a pre-commit phase between the vote and the commit. The coordinator only says 'commit' after everyone has first acknowledged 'pre-commit' β€” so before anyone commits, everyone already knows the decision is commit.
  • That extra buffer state lets a stranded participant decide safely on a timeout: reached pre-commit β†’ commit; never reached it β†’ abort. No node is ever one step away from both outcomes at once, which is what made 2PC block.
  • Under a fail-stop model (nodes only crash, the network stays connected) 3PC is non-blocking. That's the whole selling point.
  • The catch: under a network partition, 3PC can split-brain β€” one side commits, the other aborts β€” and end up inconsistent. Plus it costs a third round trip. So real systems skip it and replicate the decision with consensus (Raft/Paxos, /raft/sim) or use Sagas (/saga/sim) instead.

Start with the wound 3PC is trying to close. In two-phase commit, a participant that has voted yes is now bound: it has durably promised to commit if told to, so it holds its locks and waits. That waiting node is 'in-doubt' β€” it knows what it promised, but not what the group decided. Normally the coordinator resolves the doubt one message later by sending commit or abort.

Now crash the coordinator in exactly that gap β€” after it has collected the votes but before it announces the outcome. The in-doubt participant is trapped. It cannot commit on its own, because maybe another participant voted no and the real decision is abort. It cannot abort on its own, because maybe every vote was yes and the coordinator already made the decision commit. Either guess risks disagreeing with a peer, so the safe move is to make no move: keep the locks, keep waiting. That is what 'blocking' means β€” the protocol stops making progress, and there is no timeout it can safely take to escape.

Why the participant is genuinely stuckThe trap is that 'commit' and 'abort' are both one message away. From the in-doubt state the very next thing that happens could legitimately be either outcome, and the participant can't tell which by looking at itself. That is the precise shape of the bug. 3PC's fix is to make sure a live participant is never in a state that sits between both outcomes β€” so it can always look inward and know which way to fall. You can watch a real coordinator crash strand a participant in the two-phase commit simulator (/twopc/sim).

The fix: a pre-commit phase in the middle#

3PC keeps the same cast as 2PC β€” one coordinator, a set of participants β€” and the same goal of an all-or-nothing outcome. It just splits the single 'commit' step into two, turning two phases into three. The names are canCommit, preCommit, and doCommit.

Phase 1 β€” canCommit. The coordinator asks every participant 'can you commit?'. Each participant checks whether it is able to (constraints hold, it can acquire the locks) and votes yes or no. This is the voting phase, and it mirrors 2PC's prepare. If any vote is no, the coordinator sends abort to everyone and it's over.

Phase 2 β€” preCommit. This is the new phase, and the whole point of the protocol. If every vote was yes, the coordinator does not commit yet. Instead it sends preCommit to everyone. A participant that receives preCommit learns one crucial fact: everybody voted yes, so the decision is going to be commit. It records that it is now 'prepared to commit', acknowledges, and waits for the final go-ahead. Nothing is permanent yet β€” but everyone now knows which way this is heading.

Phase 3 β€” doCommit. Once the coordinator has collected an acknowledgement of preCommit from every participant, it sends doCommit. Each participant makes its change permanent, releases its locks, and acknowledges. The transaction is done.

Coordinatordrives the txnParticipantseach database
Phase 1 β€” canCommit / vote
canCommit?
vote YES
Phase 2 β€” preCommit (the new phase)
preCommit
now everyone knows the decision is commit
ACK preCommit
Phase 3 β€” doCommit
doCommit
make permanent, release locks
ACK
The three phases, and where the buffer sits
  1. Coordinator β†’ all: canCommit? Each participant votes YES (it can commit) or NO.
  2. Any NO β‡’ coordinator sends ABORT to everyone and stops.
  3. All YES β‡’ coordinator sends preCommit. Each participant records 'prepared to commit' β€” now knowing the decision is commit β€” and acks.
  4. All acks in β‡’ coordinator sends doCommit. Each participant commits, releases locks, and acks.
  5. The extra round (preCommit ↔ ack) is the only structural difference from 2PC. Everything 3PC gains, it gains from that round.
PredictWhat does a participant know the instant it receives preCommit that it did NOT know right after voting yes?

Hint: Think about what has to be true for the coordinator to send preCommit at all.

It knows every participant voted yes, so the decision is definitely commit β€” it just hasn't been told to make it permanent yet. Right after voting yes, a participant knows only its own vote; the group decision could still be abort if some peer voted no. But the coordinator only sends preCommit after collecting a yes from everyone. So receiving preCommit converts 'I promised to commit if asked' into 'the answer is commit'. That single piece of extra knowledge is what lets a stranded participant decide safely on a timeout instead of blocking β€” which the next section unpacks.

Why the extra phase makes it non-blocking#

Now replay the coordinator crash that froze 2PC, but with the pre-commit phase in place. A participant loses contact with the coordinator and has to decide for itself what to do. In 3PC it can, because its own state tells it the answer.

  • A participant that has received preCommit can safely commit on a timeout. Receiving preCommit means everyone voted yes, so the decision was always going to be commit. No peer can be aborting.
  • A participant that voted yes but never received preCommit can safely abort on a timeout. If it hasn't seen preCommit, then no participant can have committed yet β€” because the coordinator only sends doCommit after everyone acks preCommit. Aborting can't contradict a commit that hasn't happened.
  • In practice a survivor first runs a termination protocol β€” it asks the other reachable participants what state they are in, and the group agrees on commit if anyone reached preCommit, or abort otherwise, then elects a new coordinator to finish. The timeout rules above are the safe default each node falls back on.
The one-line reason it works3PC never lets a live participant sit in a state that is one step from both commit and abort. In 2PC the in-doubt state was exactly that dangerous middle. The preCommit phase inserts a buffer state β€” 'prepared to commit' β€” between voting yes and committing, so the states now form a clean ladder: unsure-abort, then prepared-commit, then committed. From any rung a survivor can tell which way to fall. That property, not the phase count, is what 'non-blocking' actually rests on.
Go deeperGo deeper: the state machine and Skeen's theorem

Dale Skeen formalized this in 1981. He modeled each participant as a state machine and proved a clean condition: a commit protocol is non-blocking if and only if no reachable state is adjacent to both a commit state and an abort state, and there is no state from which the protocol could go directly to either outcome depending on messages that a crash might hide. 2PC violates this β€” its 'voted yes, waiting' state is adjacent to both commit and abort. 3PC's extra phase adds an intermediate 'buffer' state (prepared-to-commit) so that the abort-reachable states and the commit-reachable states are separated by at least one step. That separation is exactly what lets an isolated node infer the outcome from its own state.

The recovery rule follows directly. On losing the coordinator, survivors run a termination protocol: gather everyone's state; if any reachable participant is in the prepared-to-commit (or committed) state, the group commits; if none is, the group aborts. A fresh coordinator is elected to drive the chosen outcome to completion. As long as failures are crash-only and every survivor can still talk to every other survivor, this always terminates with one agreed outcome β€” no one blocks.

The honest catch: network partitions break it#

Everything above assumes a fail-stop world: nodes crash outright, but the ones still alive can always reach each other. Real networks don't behave that way. A network partition can split the participants into two groups that are each alive and healthy but cannot talk across the divide. To a node on one side, a node on the other side is indistinguishable from a crashed node β€” same silence, same timeout. And that is where 3PC's safe-looking timeout turns unsafe.

The failure is a split-brain: the two sides each run the termination protocol among themselves, reach different conclusions that each look locally correct, and commit versus abort the same transaction. When the partition heals, the databases disagree. Atomicity β€” the one guarantee the whole protocol exists to provide β€” is gone.

Go deeperGo deeper: the split-brain, step by step
  1. All participants vote yes in phase 1. The coordinator starts phase 2 and sends preCommit β€” but a partition cuts the group in half mid-send.
  2. Group A did receive preCommit. Group B did not.
  3. The coordinator is now unreachable to at least one side. Both sides time out waiting for it and fall back to their termination protocols.
  4. Group A sees that its members reached prepared-to-commit, applies the 'anyone in preCommit means commit' rule, and commits.
  5. Group B sees that no member reached preCommit, applies the 'no one in preCommit means abort' rule, and aborts.
  6. Both decisions were locally safe by 3PC's own rules β€” yet the transaction is now committed on one side and aborted on the other. When the network heals, the state is inconsistent.

The root cause is that a timeout cannot distinguish 'the other side crashed' from 'the other side is alive but unreachable'. 3PC's non-blocking guarantee is built on treating a timeout as a crash. Under a partition that assumption is false, so each side confidently makes a decision that the other side contradicts. 2PC, by contrast, refuses to guess and blocks β€” it gives up liveness to keep safety. 3PC gives up safety to keep liveness, and a partition is exactly the case where that trade goes wrong.

This is why consensus wonThe fix real systems use is a quorum: require a majority to agree before committing, so a partition can have at most one side with a majority, and the minority side is forced to block rather than decide. That is precisely what Paxos and Raft do (/raft/sim). 3PC has no majority requirement β€” every reachable subgroup feels entitled to decide β€” so nothing stops two subgroups from deciding differently.

Cost: the extra round trip you pay for#

The non-blocking property is not free. The whole difference from 2PC is a third round of messages, and that round lands on the critical path β€” the transaction can't finish until it completes, and the participants hold their locks across all three phases instead of two.

Resource2PC3PCWhy the difference
Round trips23The added preCommit ↔ ack round between vote and commit
Messages (n participants)~4n~6nThree request/reply rounds instead of two β€” O(n) per phase
Latencyβ‰ˆ 2 Γ— slowest round tripβ‰ˆ 3 Γ— slowest round tripEach phase waits for the last reply; 3PC has one more phase to wait on
Lock hold timeAcross 2 phasesAcross 3 phasesLocks are held longer, so contended rows stall for longer
Blocking on crashYes (in-doubt window)No (under fail-stop)The buffer state lets survivors decide instead of waiting
You pay latency and lock time for liveness3PC buys freedom-from-blocking with a 50% increase in round trips and longer lock holds β€” and it only delivers that freedom under a failure model real networks violate. That price-to-benefit ratio, more than any single bug, is why the protocol stayed in textbooks. A third round that makes every transaction slower and every hot row more contended is a steep bill for a guarantee that a partition can void.
Trade-offs: when the extra phase is and isn't worth it

3PC's bargain is the mirror image of 2PC's. 2PC chooses safety over liveness: it would rather freeze than risk two participants disagreeing. 3PC chooses liveness over safety in the crash case: it keeps making progress, at the price of a correctness hole under partitions. Neither is a clean win, which is the honest reason the industry walked past both toward consensus.

  • Pro: non-blocking under crash-only failures β€” a coordinator crash no longer freezes prepared participants holding locks.
  • Pro: survivors can reach an outcome with a timeout-based termination protocol, so the system keeps moving.
  • Con: not partition-tolerant β€” a network split can commit on one side and abort on the other, violating atomicity.
  • Con: a third round trip on every transaction: more latency, more messages, longer lock hold times.
  • Con: it still leans on a coordinator and still assumes bounded message delays; real datacenter networks break that assumption routinely.
Where it actually fitsHonestly, almost nowhere in production. 3PC only makes sense in a setting where you can truly rule out partitions β€” a tightly controlled cluster on a reliable local network β€” and where crash-induced blocking is a real operational pain you must remove but you can't adopt full consensus. In practice, if you need non-blocking atomic commit you replicate the decision with Raft or Paxos; if you can relax atomicity you use a Saga (/saga/sim). 3PC is the option that theory explains and practice rarely picks.
2PC vs 3PC vs consensus commit vs Saga
ProtocolAtomic?Blocks on coordinator crash?Round tripsPartition-safe?Used in practice
2PCYesYes β€” in-doubt participants freeze2Safe but unavailable (blocks)XA/JTA, Postgres prepared txns, MSDTC
3PCYes (no partitions)No β€” survivors decide via timeout3No β€” can split-brainAlmost never (textbook)
Paxos/Raft CommitYesNo β€” majority survives node loss2 + consensusSafe and availableSpanner, CockroachDB, modern systems
SagaNo β€” eventual via compensationNon local txns + compensationsAvailableLong-lived microservice workflows

The row that matters is Paxos/Raft Commit. It gets 3PC's non-blocking property AND partition safety, because it decides by majority quorum instead of by 'any reachable subgroup can decide'. Under a partition, at most one side has a majority, so at most one side can act β€” the other is forced to wait. 3PC removed the block but kept the door open to split-brain; consensus closes that door. This is why modern distributed databases replicate the commit decision with a consensus group rather than reaching for a third phase.

In the wild: mostly a lesson, not a deployment

3PC is one of the most-taught, least-deployed protocols in distributed systems. Its value today is didactic: it's the clean example of how you'd try to fix blocking, and of why that fix isn't enough once you admit that networks partition. What real systems actually run instead:

  • Consensus-replicated commit is the standard modern answer. Google Spanner runs 2PC across shards, but each participant and the coordinator is itself a Paxos group, so the decision is majority-replicated and no single crash blocks it. CockroachDB and TiKV do the equivalent with Raft. This is 'Paxos Commit' (Gray & Lamport) in production β€” see the Raft simulator (/raft/sim) for the majority mechanic it relies on.
  • Sagas sidestep the whole atomic-commit problem for long-lived, multi-service workflows: each step commits locally and a failure triggers compensating actions to undo earlier steps, trading atomicity for availability (/saga/sim).
  • Classic 2PC standards (X/Open XA, JTA, Microsoft MSDTC, PostgreSQL's PREPARE TRANSACTION) are still widely used inside a trusted datacenter, blocking risk accepted β€” because the practical fix for the block is to make the coordinator's decision durable and recoverable, not to add a third phase.
  • 3PC itself shows up mainly in coursework, textbooks, and the occasional research prototype. If you find it in a production system, it's rare enough to be noteworthy.
Common misconceptions & gotchas
Does 3PC actually solve 2PC's blocking problem?

Only under a fail-stop model, where nodes crash but the survivors can always reach each other. In that world, yes β€” the pre-commit phase lets a stranded participant decide from its own state instead of freezing. But real networks partition, and under a partition 3PC can split-brain (commit on one side, abort on the other). So it trades one problem (blocking) for a worse one (inconsistency) in the exact conditions that matter most.

Why is a partition worse than a crash for 3PC?

A crash removes a node from the group; the survivors still agree with each other. A partition keeps every node alive but splits them into groups that can't communicate β€” and each group can independently reach a different 'safe' decision. A timeout can't tell 'crashed' from 'unreachable', so both sides act confidently and contradict each other. 3PC's whole safety argument assumes a timeout means a crash, and a partition breaks that assumption.

If consensus is better, why learn 3PC at all?

Because it's the clearest illustration of a deep idea: a protocol is non-blocking only if no live node sits one step from both commit and abort. 3PC shows you how to engineer that separation with a buffer state β€” and then shows you exactly why it isn't sufficient without a quorum. Understanding that gap is what makes Paxos/Raft commit click: the missing ingredient is majority agreement, not more phases.

Is the pre-commit phase the same as 2PC's prepare?

No β€” that's a common mix-up. 2PC's 'prepare' is phase 1, the voting round (canCommit in 3PC). 3PC's preCommit is a NEW second phase that happens after voting: it doesn't ask a question, it announces 'everyone voted yes, the decision is commit' and waits for acknowledgement before anyone actually commits. That announcement-before-commit is the buffer state the whole protocol turns on.

QuizAll participants vote yes. The coordinator sends preCommit, but a network partition splits the participants: group A receives preCommit, group B doesn't. The coordinator is now unreachable to both. What happens under 3PC's timeout rules?

  1. Both groups block until the partition heals, so consistency is preserved.
  2. Group A commits (it reached preCommit) and group B aborts (it didn't) β€” split-brain, and the transaction ends inconsistent.
  3. Both groups abort, because losing the coordinator always means abort.
  4. Group B asks group A over the network and they agree to commit.
Show answer

Group A commits (it reached preCommit) and group B aborts (it didn't) β€” split-brain, and the transaction ends inconsistent. β€” This is 3PC's defining failure. Group A reached prepared-to-commit, so its timeout rule says commit. Group B never saw preCommit, so its timeout rule says abort. Both act on locally correct reasoning, but they can't reach each other across the partition to reconcile, so one side commits and the other aborts β€” atomicity is violated. Option 4 is impossible precisely because a partition means the groups can't communicate. Blocking (option 1) is what 2PC would do; 3PC deliberately chooses to make progress, and that choice is what lets the split-brain happen.

In an interview

Lead with the motivation, not the phase count. 2PC blocks because a participant that voted yes is one step from both commit and abort; if the coordinator dies there, it can't safely guess. 3PC adds a pre-commit phase so that before anyone commits, everyone knows the decision is commit β€” which gives a stranded participant a safe rule: reached pre-commit means commit, didn't reach it means abort.

Then deliver the honest catch, because that's what separates a memorized answer from an understood one: 3PC is non-blocking only under a fail-stop model. Under a network partition it can split-brain β€” one side commits, the other aborts β€” because a timeout can't tell a crashed node from an unreachable one. Add the cost: a third round trip, more messages, and longer lock holds, on every transaction.

Land the conclusion interviewers actually want: this is why modern systems skip 3PC. To get non-blocking commit safely you replicate the decision with a consensus protocol (Paxos/Raft), which decides by majority quorum so at most one side of a partition can act; or you drop atomicity and use a Saga. Naming that the missing ingredient is a quorum, not another phase, shows you understand why consensus won. If pushed, mention Skeen's framing: a protocol is non-blocking only if no state is adjacent to both commit and abort.

References & further reading
References

Feedback on this topic β†’