Introduction

Imagine you're managing a bank transfer across three servers: one in New York, one in London, one in Tokyo. Every server must either all commit the transaction or all abort it — partial success is a disaster. The standard solution is Two-Phase Commit (2PC): a coordinator asks every participant "are you ready?", collects votes, then broadcasts the final decision.

2PC is elegant and widely deployed. But it has one fatal flaw: if the coordinator crashes after collecting votes but before broadcasting the decision, every participant is stuck. They each voted "yes" and locked their resources, but they have no idea whether the coordinator said "commit" or "abort." They cannot decide on their own without risking disagreement with survivors elsewhere. They must wait — potentially forever.

Three-Phase Commit (3PC), introduced by Dale Skeen in 1981, adds a single extra round called pre-commit. That one extra message is enough to break the deadlock: before the coordinator sends the final "commit", it first tells everyone "I'm about to commit." Armed with that knowledge, surviving participants can always figure out the right answer — even if the coordinator vanishes mid-protocol.

Try It: Crash the Coordinator

Below you can step through both protocols round by round. At any point click Crash coordinator to simulate a failure. Watch how 2PC blocks while 3PC recovers on its own.

<!-- {{c_html_desc}} -->
<div id="app">
  <div class="controls">
    <label class="proto-label">{{label_protocol}}</label>
    <div class="toggle-group">
      <button id="btn2pc" class="toggle active" data-proto="2pc">2PC</button>
      <button id="btn3pc" class="toggle" data-proto="3pc">3PC</button>
    </div>
    <button id="btnReset" class="ghost">{{btn_reset}}</button>
  </div>
  <div id="statusBanner" class="banner"></div>
  <div id="diagram" class="diagram">
    <div class="lane-labels">
      <div class="lane-lbl">{{label_coordinator}}</div>
      <div class="lane-lbl">A</div>
      <div class="lane-lbl">B</div>
      <div class="lane-lbl">C</div>
    </div>
    <div id="timeline" class="timeline"></div>
  </div>
  <div class="action-row">
    <button id="btnStep" class="primary">{{btn_next}}</button>
    <button id="btnCrash" class="danger">{{btn_crash}}</button>
  </div>
  <p id="explanation" class="expl"></p>
</div>
/* {{c_css_desc}} */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; font-size: 14px; color: #222; }
#app { display: flex; flex-direction: column; gap: .6rem; padding: .4rem; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.proto-label { font-weight: 600; }
.toggle-group { display: flex; border: 1px solid #1d3557; border-radius: 8px; overflow: hidden; }
.toggle { border: none; padding: .3rem .7rem; cursor: pointer; background: #fff; color: #1d3557; font-weight: 600; font-size: 13px; }
.toggle.active { background: #1d3557; color: #fff; }
button.ghost { background: #fff; color: #1d3557; border: 1px solid #1d3557; border-radius: 8px; padding: .3rem .7rem; cursor: pointer; font-size: 13px; }
button.primary { background: #1d3557; color: #fff; border: 1px solid #1d3557; border-radius: 8px; padding: .4rem .9rem; cursor: pointer; font-weight: 600; font-size: 13px; }
button.danger { background: #c92f3c; color: #fff; border: 1px solid #c92f3c; border-radius: 8px; padding: .4rem .9rem; cursor: pointer; font-weight: 600; font-size: 13px; }
button:disabled { opacity: .45; cursor: default; }
.banner { min-height: 1.6em; font-weight: 700; font-size: .92rem; border-radius: 6px; padding: .25rem .6rem; }
.banner.ok { background: #d4edda; color: #0a7d33; }
.banner.bad { background: #fce4e6; color: #c92f3c; }
.banner.warn { background: #fff3cd; color: #856404; }
/* {{c_css_diagram}} */
.diagram { border: 1px solid #cdd9e3; border-radius: 8px; overflow: hidden; background: #f9fbfc; }
.lane-labels { display: grid; grid-template-columns: 80px repeat(3, 1fr); background: #e4eaf0; border-bottom: 1px solid #cdd9e3; }
.lane-lbl { text-align: center; font-weight: 700; font-size: 12px; padding: .3rem; color: #1d3557; }
.lane-lbl:first-child { border-right: 2px solid #1d3557; font-size: 11px; }
.timeline { padding: .4rem .3rem; display: flex; flex-direction: column; gap: 3px; max-height: 240px; overflow-y: auto; }
.row { display: grid; grid-template-columns: 80px repeat(3, 1fr); align-items: center; min-height: 28px; }
.row-label { font-size: 11px; color: #5a7088; padding-right: .3rem; text-align: right; border-right: 2px solid #e4eaf0; }
.msg { display: flex; align-items: center; justify-content: center; }
.pill { display: inline-block; font-size: 11px; font-weight: 700; border-radius: 12px; padding: 2px 7px; white-space: nowrap; }
.pill.send { background: #dbeafe; color: #1e40af; border: 1px solid #93c5fd; }
.pill.recv { background: #dcfce7; color: #166534; border: 1px solid #86efac; }
.pill.crash { background: #fce4e6; color: #c92f3c; border: 1px solid #f9a8b0; }
.pill.recover { background: #fff3cd; color: #856404; border: 1px solid #fcd34d; }
.action-row { display: flex; gap: .5rem; }
.expl { font-size: .88rem; color: #444; line-height: 1.5; min-height: 2.8em; }
// Code not found

The key insight: in 3PC, if a participant has seen pre-commit, it knows every other participant voted "yes" (the coordinator only sends pre-commit after all votes are in). So survivors can safely commit without waiting. In 2PC there is no such guarantee — a participant that voted "yes" cannot tell whether others voted "yes" too.

The Real Complexity

3PC looks like a clean solution. Why isn't it everywhere?

  • It solves crash failures. Under the crash-stop model — processes fail by halting, messages eventually arrive — 3PC is provably non-blocking. Skeen (1981) proved that three phases are both necessary and sufficient: you cannot do it in two, and you do not need four.
  • It does not solve network partitions. If the network splits into two groups that cannot talk to each other, both groups might independently decide — one commits, the other aborts. This is the split-brain problem, and no deterministic protocol can avoid it.
  • The FLP impossibility theorem (Fischer, Lynch, Paterson, 1985) proves that in a fully asynchronous system, no protocol can guarantee both safety and liveness in the presence of even a single crash. 3PC sidesteps this by assuming partial synchrony — messages are slow but eventually arrive.
  • Extra message cost. 2PC requires 2n2n messages per round; 3PC requires 3n3n. For high-throughput systems that overhead is real.
  • Modern alternatives like Paxos and Raft trade the clean three-phase structure for quorum-based decisions that tolerate both crashes and partitions, at the cost of more complexity.

3PC is not the final answer, but it is the cleanest demonstration of a fundamental idea: one extra bit of shared knowledge can transform a blocking protocol into a non-blocking one.

Where It Matters

The problem 3PC solves — how do independent nodes agree on an irreversible action without blocking forever? — is one of the deepest in distributed computing:

  • Distributed databases: every multi-shard transaction (think cross-bank wire transfer, e-commerce checkout spanning warehouses) needs an atomic commit protocol. Systems like Google Spanner and CockroachDB use variants of Paxos precisely because pure 3PC cannot handle partitions.
  • Consensus algorithms: Paxos and Raft can be understood as 3PC-style protocols generalized to handle partitions via quorums. The pre-commit phase maps directly onto Paxos's "prepare/promise" round.
  • Blockchain finality: proof-of-stake finality gadgets (Casper, Tendermint) are essentially Byzantine-fault-tolerant consensus — the same problem, with adversarial failures on top of crashes.
  • Saga patterns in microservices: modern services avoid 2PC by using compensating transactions (sagas). Understanding why 2PC blocks is the motivation for the entire saga pattern.

See also: the halting problem for the broader theme of undecidability in computation, and P vs NP for why some decision problems stay hard no matter the algorithm.

Conclusion

Three-Phase Commit is a beautiful illustration of how a tiny change in protocol design can cross a fundamental threshold. Two phases leave participants unable to decide alone when the coordinator vanishes. Three phases — by broadcasting "I am about to commit" before the final word — give every participant enough shared knowledge to finish the job independently.

The catch is that "enough shared knowledge" works only when the network itself is reliable enough to eventually deliver messages. The moment the network can partition, you need quorums, and you are back in the realm of Paxos and Raft. That boundary — crash-stop versus partition tolerance — is where the FLP impossibility theorem lives, and it remains one of the most surprising results in all of computer science.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/three-phase-commit/Content licensed under CC BY-NC 4.0.