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.

<!-- Demo layout: protocol toggle, status banner, message-flow diagram, step/crash buttons, explanation text -->
<div id="app">
  <div class="controls">
    <label class="proto-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">Reset</button>
  </div>
  <div id="statusBanner" class="banner"></div>
  <div id="diagram" class="diagram">
    <div class="lane-labels">
      <div class="lane-lbl">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">Next step →</button>
    <button id="btnCrash" class="danger">Crash coordinator</button>
  </div>
  <p id="explanation" class="expl"></p>
</div>
/* Styles for the 3PC vs 2PC interactive demo */
* { 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; }
/* Message-flow diagram grid */
.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; }
// Step-through simulation of 2PC and 3PC with coordinator crash

// 2PC has 2 phases: vote (PREPARE / VOTE-YES) and commit (COMMIT / ACK)
const STEPS_2PC = [
  { phase: 'Vote',    from: 0, to: [1,2,3], msg: 'PREPARE',     dir: 'send', expl: 'Coordinator → participants: PREPARE. Each participant locks its resource and gets ready to vote.' },
  { phase: 'Vote',    from: [1,2,3], to: 0,  msg: 'VOTE-YES',   dir: 'recv', expl: 'All participants voted YES. Coordinator now holds all votes but has NOT yet told anyone.' },
  { phase: 'Commit',  from: 0, to: [1,2,3], msg: 'COMMIT',      dir: 'send', expl: 'Coordinator → participants: COMMIT. If the coordinator crashes NOW, participants are stuck — they voted yes but don’t know the decision.' },
  { phase: 'Commit',  from: [1,2,3], to: 0,  msg: 'ACK',        dir: 'recv', expl: 'All ACKs received. Transaction committed. Coordinator can release its log.' },
];

// 3PC has 3 phases: vote, pre-commit, and commit
const STEPS_3PC = [
  { phase: 'Vote',       from: 0, to: [1,2,3], msg: 'PREPARE',     dir: 'send', expl: 'Coordinator → participants: PREPARE. Same first step as 2PC.' },
  { phase: 'Vote',       from: [1,2,3], to: 0,  msg: 'VOTE-YES',   dir: 'recv', expl: 'All participants voted YES. Now comes the extra 3PC step.' },
  { phase: 'Pre-commit',  from: 0, to: [1,2,3], msg: 'PRE-COMMIT',  dir: 'send', expl: 'Coordinator → participants: PRE-COMMIT. This is the key extra message. Every participant now knows ALL others voted yes. If coordinator crashes after this point, survivors can safely commit on their own.' },
  { phase: 'Pre-commit',  from: [1,2,3], to: 0,  msg: 'ACK',        dir: 'recv', expl: 'All participants ACKed PRE-COMMIT. Coordinator can now safely send the final COMMIT.' },
  { phase: 'Commit',     from: 0, to: [1,2,3], msg: 'COMMIT',      dir: 'send', expl: 'Coordinator → participants: COMMIT. Transaction finalised.' },
  { phase: 'Commit',     from: [1,2,3], to: 0,  msg: 'ACK',        dir: 'recv', expl: 'All ACKs received. Transaction committed.' },
];

const PARTICIPANT_NAMES = ['A','B','C'];
let proto = '2pc';
let stepIndex = 0;
let crashed = false;
let done = false;

function steps(){ return proto === '2pc' ? STEPS_2PC : STEPS_3PC; }

function renderRow(step, highlight){
  // Build one timeline row for a protocol step
  const row = document.createElement('div');
  row.className = 'row' + (highlight ? ' current' : '');
  const lbl = document.createElement('div');
  lbl.className = 'row-label';
  lbl.textContent = step.phase;
  row.appendChild(lbl);

  for(let i = 0; i < 3; i++){
    const cell = document.createElement('div');
    cell.className = 'msg';
    const from = step.from, to = step.to;
    const fromCoord = from === 0;
    const toArr = Array.isArray(to) ? to : [to];
    const toCoord = toArr.includes(0);
    const involvesParticipant = fromCoord ? toArr.includes(i+1) : (Array.isArray(from) ? from.includes(i+1) : from === i+1);
    if(involvesParticipant){
      const pill = document.createElement('span');
      pill.className = 'pill ' + step.dir;
      pill.textContent = step.msg;
      cell.appendChild(pill);
    }
    row.appendChild(cell);
  }
  return row;
}

function renderCrashRow(atStep){
  // Build a crash indicator row
  const row = document.createElement('div');
  row.className = 'row';
  const lbl = document.createElement('div');
  lbl.className = 'row-label';
  lbl.textContent = '💥';
  row.appendChild(lbl);
  for(let i=0;i<3;i++){
    const cell = document.createElement('div');
    cell.className = 'msg';
    const pill = document.createElement('span');
    pill.className = 'pill crash';
    pill.textContent = 'CRASHED';
    cell.appendChild(pill);
    row.appendChild(cell);
  }
  return row;
}

function renderRecoverRow(canRecover){
  // Build a recovery outcome row (green=can decide, red=blocked)
  const row = document.createElement('div');
  row.className = 'row';
  const lbl = document.createElement('div');
  lbl.className = 'row-label';
  lbl.textContent = '🔄';
  row.appendChild(lbl);
  for(let i=0;i<3;i++){
    const cell = document.createElement('div');
    cell.className = 'msg';
    const pill = document.createElement('span');
    pill.className = canRecover ? 'pill recover' : 'pill crash';
    pill.textContent = canRecover ? 'COMMIT (decided)' : 'BLOCKED';
    cell.appendChild(pill);
    row.appendChild(cell);
  }
  return row;
}

function redrawTimeline(){
  const tl = document.getElementById('timeline');
  tl.innerHTML = '';
  const allSteps = steps();
  for(let i = 0; i < stepIndex; i++){
    tl.appendChild(renderRow(allSteps[i], false));
  }
  if(crashed){
    tl.appendChild(renderCrashRow(stepIndex));
    // Recovery rule: 3PC recovers if coordinator crashed AFTER sending PRE-COMMIT (stepIndex>=3); 2PC always blocks.
    // 3PC: if coordinator crashed AFTER sending PRE-COMMIT (step index >= 3),
    // participants saw pre-commit and can commit on their own.
    // 2PC: participants can never safely decide on their own.
    const canRecover = proto === '3pc' && stepIndex >= 3;
    tl.appendChild(renderRecoverRow(canRecover));
  }
  tl.scrollTop = tl.scrollHeight;
}

function setBanner(text, cls){
  const b = document.getElementById('statusBanner');
  b.textContent = text;
  b.className = 'banner ' + cls;
}

function setExpl(text){
  document.getElementById('explanation').textContent = text;
}

function updateButtons(){
  const btnStep = document.getElementById('btnStep');
  const btnCrash = document.getElementById('btnCrash');
  btnStep.disabled = done || crashed;
  btnCrash.disabled = done || crashed || stepIndex === 0;
}

function advance(){
  if(done || crashed) return;
  const allSteps = steps();
  if(stepIndex < allSteps.length){
    setExpl(allSteps[stepIndex].expl);
    stepIndex++;
    redrawTimeline();
    if(stepIndex === allSteps.length){
      done = true;
      setBanner('✓ Transaction committed successfully.', 'ok');
      setExpl('All participants acknowledged the final COMMIT. The transaction is durable and complete.');
    }
  }
  updateButtons();
}

function crashCoordinator(){
  if(done || crashed || stepIndex === 0) return;
  crashed = true;
  redrawTimeline();
  const canRecover = proto === '3pc' && stepIndex >= 3;
  if(canRecover){
    setBanner('⚠ Coordinator crashed — but 3PC survivors recovered and committed.', 'warn');
    setExpl('Because participants already received PRE-COMMIT, they know everyone voted YES. They elect a new coordinator and commit unanimously — no data loss, no blocking.');
  } else {
    setBanner('✗ Coordinator crashed — participants are blocked (cannot decide safely).', 'bad');
    setExpl(proto === '2pc' ? 'Participants voted YES and locked their resources, but they have no idea whether the coordinator said COMMIT or ABORT before crashing. They must wait for the coordinator to recover — potentially forever.' : 'The coordinator crashed before sending PRE-COMMIT. Participants cannot tell if all votes were YES, so they must wait for recovery just like in 2PC.');
  }
  updateButtons();
}

function reset(){
  stepIndex = 0; crashed = false; done = false;
  redrawTimeline();
  setBanner('', '');
  setExpl('Choose a protocol above, then click ’Next step’ to advance. Click ’Crash coordinator’ at any point to simulate a failure.');
  updateButtons();
}

function selectProto(p){
  proto = p;
  document.getElementById('btn2pc').classList.toggle('active', p === '2pc');
  document.getElementById('btn3pc').classList.toggle('active', p === '3pc');
  reset();
}

document.getElementById('btnStep').addEventListener('click', advance);
document.getElementById('btnCrash').addEventListener('click', crashCoordinator);
document.getElementById('btnReset').addEventListener('click', reset);
document.getElementById('btn2pc').addEventListener('click', () => selectProto('2pc'));
document.getElementById('btn3pc').addEventListener('click', () => selectProto('3pc'));

reset();
Result
Console

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.