The full picture
🎬 Two-phase commit: the gap where it blocks
1 / 5One business transaction, three services, three separate databases. There is no shared lock manager, so 'all commit or none do' has to be negotiated over the network — and two-phase commit is that negotiation, run by a coordinator.
| Phase | Coordinator | Participant | What can go wrong |
|---|---|---|---|
| 1 — prepare | asks all, collects votes | does the work, locks, force-writes prepare, votes | a NO or a timeout → global abort, which is safe |
| 2 — decide | force-writes the outcome, then broadcasts | applies the outcome, releases locks | coordinator dies here → participants block |
| Recovery | reads its log, re-broadcasts | asks the coordinator, or waits | coordinator log lost → only heuristic (manual) resolution |
- The asymmetry is the whole protocol: aborting is always safe, so a timeout in phase 1 can be resolved unilaterally — abort. Committing is never safe to guess, so a timeout in phase 2 cannot. That's why the blocking window is exactly the interval in which some participant has voted YES and does not yet know the outcome.
- A prepared transaction is a live transaction holding locks, and that is far worse than it sounds. In Postgres, an orphaned
PREPARE TRANSACTIONholds its locks and pins the vacuum horizon indefinitely — table bloat that no amount of tuning fixes. This is precisely whymax_prepared_transactionsdefaults to 0 and you must deliberately turn the feature on. - Heuristic decisions are the operational reality: when an operator is tired of waiting, XA lets a participant resolve on its own —
XA_HEURCOM(committed anyway) orXA_HEURRB(rolled back anyway). The transaction manager then reports a heuristic mixed outcome, which means: the atomicity guarantee is gone and a human must reconcile. Any answer that mentions heuristic outcomes signals you've run this in production. - Blocking is not a bug in 2PC — it's a theorem. A non-blocking atomic commit protocol requires a perfect failure detector, which an asynchronous network cannot give you; atomic commit is as hard as consensus. 3PC adds a pre-commit phase and is non-blocking under crash-stop with bounded delays, but it breaks under network partitions (both sides can decide differently), which is exactly the failure that actually happens. That's why nobody ships 3PC.
- Where you'll still legitimately meet 2PC:
XA/JTA with Atomikos or Narayana in enterprise stacks (broker + database in one trust domain), PostgresPREPARE TRANSACTION, MySQL XA, and — most importantly — inside modern distributed databases. Spanner, CockroachDB, TiDB and YugabyteDB all run 2PC; they just run it across Paxos/Raft groups rather than single nodes, which is the trick that defuses it.
// Phase 1: the participant does EVERYTHING except make it visible.
tx.doWork(); // rows written, constraints checked
tx.lock(); // locks acquired and HELD until phase 2
log.forceWrite(PREPARED); // fsync #1 — survives a crash, so the promise is durable
vote(YES); // from here on it may NOT abort on its own
// ---- coordinator decides, force-writes its own log, broadcasts ----
// Phase 2:
log.forceWrite(COMMITTED); // fsync #2
tx.applyAndReleaseLocks();
// Two round trips and two fsyncs per participant, and locks are held across
// BOTH — so p99 latency is the slowest participant, and lock contention is the
// slowest participant plus a network round trip. That cost, not the protocol's
// complexity, is why 2PC does not scale out.