Two-Phase Commit (and Why 3PC Exists)
The Coordinate Move, Taken Seriously
The Distributed Transaction Problem left you with three moves for a change that spans two independent stores: coordinate, compensate, or avoid it. This lesson takes the first one seriously. If the trouble is that two banks each commit on their own — and a crash between the two commits loses the money — then the obvious fix is to stop letting them commit on their own. Appoint a coordinator whose entire job is to make sure everyone does the same thing: everyone commits, or everyone aborts. Nobody acts alone.
That protocol is two-phase commit (2PC), and it genuinely works — it delivers real, cross-machine atomicity, the all-or-nothing that single-node ACID (ACID, Precisely) could never give you across independent databases. It is not vaporware or a toy: Postgres implements it, Java's XA standard is built on it, and distributed databases use it internally. In this lesson we'll run it on two real Postgres instances and watch $100 move atomically between them.
But #48 also warned you that this move has a blocking flaw, and that warning is the reason the lesson exists. So we'll do both: build the protocol until it works, then find the exact crack where it freezes solid — and see why the "fix" for that crack (three-phase commit) is a fix almost nobody uses. The shape of the whole lesson is: 2PC buys you atomicity, and the currency it pays in is availability.

Phase 1: Prepare — the Binding Promise
The name says it: two phases. The first is PREPARE, and it's a vote. The coordinator sends every participant the same question — "can you commit?" — and each participant does something stronger than it sounds. It does all the work (applies the change in its own transaction), forces that work durably to disk in a special prepared state, keeps holding all its locks, and only then votes YES. That YES is not casual; it is a binding promise: "I have done the work, it is safe on disk, and I give up my right to abort on my own — if you tell me to commit, I will, even after a crash." (A participant that can't do the work — a constraint violation, a disk error — votes NO instead.)
Postgres exposes exactly this with a real command, PREPARE TRANSACTION. We ran it on Bank A and Bank B (two separate machines):
-- Bank A -- Bank B
BEGIN; BEGIN;
UPDATE accounts SET balance=balance-100 UPDATE accounts SET balance=balance+100
WHERE name='alice'; WHERE name='bob';
PREPARE TRANSACTION 'xfer'; PREPARE TRANSACTION 'xfer';
After PREPARE TRANSACTION, each bank is prepared and in-doubt — you can see it sitting in the pg_prepared_xacts view. Two things are now true and both matter enormously. First, the change is durable but invisible: Alice's new balance is written to disk, but no other transaction can see it yet — it isn't committed. Second, the locks are still held. The row is frozen. This is the price you're quietly paying from the very first phase: every prepared participant is sitting on its locks, blocking anyone else who wants those rows, for the entire duration of the protocol. On a busy row, that alone can wreck throughput (Pessimistic vs Optimistic Locking — a held lock is a queue). Hold that thought; it's about to get much worse.
Phase 2: Commit — the Point of No Return
Once the coordinator has collected the votes, it makes the decision, and this is the hinge of the whole protocol. The rule is simple: if every participant voted YES, commit; if anyone voted NO (or didn't answer), abort. But the crucial part is what the coordinator does with that decision — it writes it to its own durable log first. That log record is the point of no return. The instant "commit" is on disk, the transaction is committed as far as the universe is concerned, even if every participant is momentarily unreachable; the coordinator will keep telling them until they all comply. The coordinator's log is the single source of truth for whether this transaction happened.
Then it broadcasts the decision, and the participants obey — turning their durable prepared state into a real commit and finally releasing their locks. In Postgres, that's COMMIT PREPARED (or ROLLBACK PREPARED for the abort branch):
-- coordinator decided COMMIT (both voted YES); tell everyone:
COMMIT PREPARED 'xfer'; -- on Bank A
COMMIT PREPARED 'xfer'; -- on Bank B
And it works — really works. After phase 2, Bank A alice = 0, Bank B bob = 100, total still $100 — the money moved atomically across two independent machines, the thing #48 proved was impossible with naive commits. That is 2PC delivering its promise: nobody committed until everybody had promised, so there is no window where one bank is debited and the other isn't. It looks airtight. To find the crack, you have to ask the uncomfortable question: what if the coordinator dies at the worst possible moment?
The Crack: In-Doubt and Blocked
Rewind to the gap between the two phases. Both banks have voted YES — they are prepared, in-doubt, holding their locks — and they are waiting for the coordinator to tell them commit or abort. Now the coordinator crashes, and the decision never comes.
Think about what a stranded bank can do. It cannot commit on its own: maybe a different participant voted NO, which means the correct outcome is abort, and committing would break atomicity. It cannot abort on its own: maybe everyone voted YES and the coordinator already wrote "commit" to its log — aborting would also break atomicity. It has genuinely no safe unilateral move. So it does the only correct thing: it holds its locks and waits — for a coordinator that may take minutes, hours, or a human to come back. This is the blocking flaw, and it is not a rare corner case; it is baked into the protocol.
We reproduced it exactly. Bank A prepared (PREPARE TRANSACTION 'xfer2'), then we killed the coordinator before the decision. The transaction just… sits there in pg_prepared_xacts, holding alice's row lock. An ordinary, unrelated customer transaction that so much as touches that row gets this:
ERROR: canceling statement due to lock timeout
It's blocked. And here's the part that makes it visceral: restarting Bank A does not help. A prepared transaction is durable — it survives the reboot, still sitting in pg_prepared_xacts, still holding the lock. The row is frozen across restarts, and the only thing that can free it is the coordinator's decision finally arriving (COMMIT PREPARED or ROLLBACK PREPARED).
Notice precisely what did and didn't go wrong, because it's the opposite of #48's naive transfer. No money was lost. No money was duplicated. Nothing is inconsistent. 2PC kept its atomicity promise perfectly. What it gave up instead is availability: the accounts are frozen — unusable — until the coordinator returns. That is the whole trade, and it's CAP, Properly made concrete: when it can't reach agreement, 2PC chooses consistency and sacrifices availability. It never loses data. It just stops. And because the coordinator is the single thing that can un-freeze everyone, it is a glaring single point of failure.

Break It Yourself
The blocking window is easy to state and hard to feel, so drive the protocol yourself. Step the coordinator through it — send the prepare, watch both banks vote and lock, let it decide and deliver — and see the clean atomic commit. Then reset, and this time crash the coordinator at a different moment.
Crash it before the banks prepare and nothing is at stake — they simply abort, harmless. Crash it in the window — after both have voted YES but before they hear the decision — and watch both banks flip to in-doubt and frozen, holding their locks, with nowhere to go. Flip a bank's vote to NO and watch the clean-abort branch instead. Play with every timing until the shape is obvious: there is a specific, unavoidable interval where one crash freezes everyone, and no amount of local cleverness gets a stranded bank out of it. That frozen interval is the entire reason people reach for other answers.

Why 3PC Exists (and Why You've Never Used It)
The blocking flaw is so annoying that people designed a fix: three-phase commit (3PC). The idea is to insert an extra pre-commit phase between the vote and the commit. Now, before anyone actually commits, the coordinator first tells everyone "you all voted yes, get ready" — and the trick is that this extra step lets a stranded participant reason safely on its own using a timeout. If a participant is waiting and hears nothing, it can look at which phase it reached and make the safe unilateral choice (abort if it never got the pre-commit; commit if it did). So 3PC is non-blocking under a crash — it fixes exactly the freeze we just watched.
So why have you almost certainly never used it? Because 3PC only works if the network behaves. Its safety depends on bounded message delays and no partitions — timeouts have to mean "the other node is dead," not "the other node is fine but the network is slow." On a real network those are indistinguishable (the Two Generals problem from #48), and under a genuine network partition 3PC can split-brain: one side times out and aborts while the other side proceeds to commit — inconsistency, which is worse than blocking. So 3PC trades 2PC's blocking for a partition-vulnerability, adds a whole extra round of messages and latency, and buys you protection only against the failure mode (a clean crash) that's easiest to handle anyway. That's a bad deal on a partition-prone network, which is why 3PC lives in textbooks, not production.
And there's a deeper reason to stop chasing this: it's provably a dead end. There is no atomic-commit protocol that is non-blocking under both node crashes and network partitions (Skeen). You cannot have it all. The genuine escape isn't a smarter commit protocol at all — it's consensus (Paxos, Raft; a topic for later). Consensus wins by dropping the requirement that everyone agree: instead of needing a unanimous vote (which one dead node can stall forever), it needs only a majority quorum, so it can make progress while a minority is crashed or partitioned away. That quorum trick — not a third phase — is what modern distributed databases actually use to commit.
When to Actually Use 2PC — and What to Remember
Given all that, when should you reach for two-phase commit? Rarely, and deliberately. It's the right tool when you genuinely need cross-node atomicity, you control all the participants, they live close together on a reliable network, and you can tolerate the blocking risk — which is exactly the situation inside a distributed database or across a small set of databases via XA. It is the wrong tool for stitching together independent services over the open network, where it couples everyone's fate: any one participant down blocks all of them, and holding locks across the network murders throughput under contention. That's why most systems, faced with #48's three moves, quietly pick one of the other two.
The takeaways:
- 2PC is the "coordinate" move. A coordinator runs two phases: PREPARE (everyone does the work, forces it durable, holds locks, and votes — a binding promise; Postgres
PREPARE TRANSACTION) and COMMIT (the coordinator logs the decision — the point of no return — then tells everyone;COMMIT PREPARED). It delivers real cross-node atomicity (measured: $100 moved atomically across two Postgres instances). - The price is blocking. If the coordinator crashes in the window after the votes but before the decision, participants are stuck in-doubt, holding locks, unable to commit or abort alone (measured:
canceling statement due to lock timeout, and it survives a restart). It never loses data — it stops: 2PC picks consistency over availability (CAP, Properly), and the coordinator is a single point of failure. - 3PC "fixes" blocking but breaks on partitions (split-brain), so it's essentially unused. No commit protocol is non-blocking under both crashes and partitions; the real fix is consensus (quorum, not unanimity).
- Use 2PC sparingly — controlled participants, reliable network, atomicity truly required — and prefer to avoid the distributed transaction or to compensate instead. Next — Saga: The Data-Consistency Mechanics: the second move — give up on locking and atomicity entirely, do each step for real, and undo the earlier ones when a later step fails.