The question this answers
How can three independent databases be made to commit or abort as one, and what exactly does that cost?
Atomic commit: every participant reaches the same decision (all commit, or all abort), and no participant commits unless every participant voted yes. This is a safety guarantee only. 2PC guarantees no liveness — it does not promise the decision is ever reached, and a participant that voted yes may wait indefinitely. Isolation is *not* provided by 2PC; it comes from whatever locking each participant already does, held longer.
Everything below is bought to hold this sentence. "Strongly consistent" with no scope attached is a slogan, not a guarantee — read what it actually covers, and what it explicitly does not.
A participant that has voted YES knows it has durably prepared and that it may no longer abort unilaterally. It does *not* know the global decision, and cannot compute it: it knows only its own vote. The coordinator, after logging its decision, knows the outcome — and is the only node that does. That asymmetry is the whole protocol: exactly one node holds the truth, and everyone else is waiting to be told.
A node knows its own state and the messages that arrived. Everything else is inference from evidence that was already stale. "B has not replied in five seconds" is knowledge; "B is down" is a decision — and usually the bug.
The protocol, and where the promise is made
Phase one is a question: the coordinator asks every participant "can you commit?". A participant that answers YES must first make its work durable — write the changes and the undo information to stable storage — so that it can honour a commit decision even across a crash. That durable state is called prepared, and it is not a normal state for a database transaction to be in: it holds locks, it is invisible to readers, and it cannot be aborted by the participant that owns it.
Phase two is a statement. Once every vote is in, the coordinator makes the decision, writes it to its own log, and only then broadcasts it. The order matters more than anything else in the protocol: the decision is durable before it is communicated, so a coordinator that crashes mid-broadcast can recover and finish telling everyone. If it broadcast first and logged second, a crash could leave some participants committed and the coordinator with no memory of why.
A single NO vote, or a vote that never arrives before the coordinator’s timeout, produces a global ABORT. The asymmetry is deliberate: the coordinator may unilaterally decide to abort while it is still collecting votes, because nobody has committed yet. This is the *presumed abort* optimisation — no log record is needed for an abort decision, since a participant asking about a transaction the coordinator has never heard of can safely be told "abort".
PREPARED is a state your intuition does not have
Every mental model of a transaction has two ends: running (can still abort) and finished (committed or rolled back). 2PC inserts a third state between them, and almost every surprise about the protocol comes from that state being unfamiliar.
A prepared transaction has *no owning session*. In PostgreSQL, PREPARE TRANSACTION disconnects the transaction from the backend that created it — you can close the connection, restart the client, and the transaction is still there, holding its locks, waiting. It survives a database restart. It appears in pg_prepared_xacts and in nothing else you normally look at. It holds back the transaction horizon, so autovacuum cannot clean up rows, and a forgotten prepared transaction will bloat tables it never touched.
That durability is exactly what makes the guarantee possible — a participant must be able to honour its YES after a crash — and exactly what makes the failure mode severe. The protocol has, by design, created state that cannot be cleaned up without knowing the global decision.
postgres=# SELECT gid, prepared, owner, database FROM pg_prepared_xacts;
gid | prepared | owner | database
------------------------------+-------------------------------+-------+----------
xa-7f1c9e04-orders-482913 | 2026-08-24 03:11:47.223+00 | app | orders
(1 row)
-- 19 hours old. Locks still held. Autovacuum blocked on this table since.
postgres=# SELECT count(*) FROM pg_locks WHERE virtualtransaction LIKE '-1/%';
count
-------
146What 2PC is not
It is not consensus. Consensus protocols such as Raft tolerate the failure of a minority of nodes and still make progress, because any majority can form a decision. 2PC requires *unanimity* to commit and requires the coordinator specifically to be alive to decide. One participant failing at the wrong moment stalls it; the coordinator failing at the wrong moment stalls it harder. Compare What Consensus Actually Solves — the two protocols answer different questions, and 2PC is the weaker of the two in exactly the way that matters under failure.
It does not give you isolation across participants. 2PC coordinates the *commit*, not the reads. If your participants use two-phase locking and hold locks from first access through prepare to commit, you get serializable behaviour across the whole transaction as a side effect. If your participants use snapshot isolation, each takes its own snapshot at its own moment, and a reader can observe one participant’s changes before the other’s — a distributed read skew that 2PC does nothing about. Spanner adds a global timestamp precisely to close this gap.
It does not make the operation more available. The transaction now requires every participant *and* the coordinator to be reachable simultaneously. You have combined the availability of N systems multiplicatively and added a new single point of stall.
| Two-phase commit | Consensus (Raft/Paxos) | |
|---|---|---|
| Decision ruleprotocol | Unanimous — any NO aborts | Majority — a minority may be down |
| Tolerates participant crashprotocol | No — stalls or aborts | Yes, up to a minority |
| Tolerates coordinator crashprotocol | No — participants block | Yes — elects a new leader |
| Question answeredprotocol | Did everyone agree to commit? | What is the next agreed value? |
| Round trips to decidetypical | 2 (prepare, commit) | 1 after a stable leader |
Where it is genuinely the right tool
The reputation of 2PC as an anti-pattern comes from a specific abuse: using XA to span services owned by different teams over a WAN, with transactions that stay open for user-scale durations. That is a bad idea for reasons that have little to do with the protocol.
Inside one administrative domain, with a reliable coordinator, low-latency links and transactions that last milliseconds, 2PC is excellent and is used constantly — you are almost certainly relying on it right now. A sharded database committing a multi-shard write runs 2PC across its shards. Spanner runs 2PC across Paxos groups, with each participant being a replicated group rather than a single machine, which removes the participant-failure problem. FoundationDB, CockroachDB and TiDB all commit distributed transactions with recognisably two-phase protocols. A database with a WAL and a replicated log is doing an internal version of the same handshake.
The honest rule: 2PC is appropriate when you control every participant, when the uncertainty window is short enough that blocking is survivable, and when you can make the coordinator itself fault-tolerant. Break any of those and you are choosing a saga whether you admit it or not.
- One trust domain — you can restart a stuck participant and read its logs.
- Short transactions — the lock window is milliseconds, not the duration of a user session.
- Low latency between participants — the prepare round trip is not crossing a continent (The One Number You Cannot Optimise).
- A fault-tolerant coordinator — replicated via consensus, not a single process with a local log file.
- Participants that genuinely implement prepare — not a wrapper that fakes it by doing the work early.
Key points
- Phase one collects durable promises; phase two announces a decision that was logged before it was sent.
- A YES vote surrenders the participant’s right to abort — that surrender is what makes atomicity possible.
- PREPARED is a third transaction state that survives crashes and disconnections, holds locks, and cannot be resolved locally.
- 2PC is not consensus: it needs unanimity to commit and a live coordinator to decide, so it has no fault tolerance.
- It coordinates commit, not isolation — snapshot-isolated participants can still show a reader a skewed cross-service view.
- Inside one trust domain with short transactions and a replicated coordinator it is a fine, widely used tool.
The chain, answered
Every field here is required, which is why no lesson in this domain can recommend a design without naming what an operator sees when it fails, what survives the partition, what repairs it afterwards, and the simpler thing to consider first.
- • The coordinator assigns a global transaction id and records the participant list.
- • PREPARE: each participant executes its work, forces undo and redo information to stable storage, and replies YES or NO. A YES means "I can commit even if I crash now".
- • The coordinator collects votes. Any NO, or any missing vote at its timeout, yields ABORT.
- • The coordinator forces its decision to its own log. This write is the commit point of the whole transaction.
- • COMMIT or ABORT is broadcast; each participant applies the decision, releases locks, and acknowledges.
- • Once all acks arrive, the coordinator may forget the transaction. Until then it must remember, because a participant may ask again.
- • A PREPARE is lost, so a participant never prepares and the coordinator times out into ABORT.
- • A YES vote is lost, so the coordinator aborts a transaction every participant could have committed.
- • The coordinator crashes before logging its decision — recoverable as abort, since nobody committed.
- • The coordinator crashes *after* logging COMMIT but before broadcasting it — the participants block (The Blocking Window: When 2PC Stops and Waits).
- • A participant crashes while prepared; on restart it must re-read its log and ask the coordinator what happened.
- • A COMMIT message is delivered twice, so participants must make applying a decision idempotent.
- • Lock pile-up during prepare: the operator sees lock waits and connection-pool exhaustion on a participant that itself is healthy — the queue is behind transactions waiting for a coordinator on another machine.
- • Abort storms from a slow participant: one participant’s p99 exceeds the coordinator’s vote timeout, so a fraction of all transactions abort with no error logged anywhere except the coordinator. Application error rate rises with no obvious cause.
- • Prepared-transaction leak:
pg_prepared_xactsorXA RECOVERshows entries hours old. Autovacuum is blocked, table bloat grows, and the symptom surfaces first as degrading query performance, not as a transaction problem. - • Throughput collapse under contention: because locks are now held for the full two-phase round trip, a hot row that supported thousands of transactions per second locally supports tens.
- • Two round trips of coordination before any effect is visible: prepare/vote, then decide/apply.
- • Latency floor is the slowest participant’s prepare plus the coordinator’s log flush plus two network hops — every transaction pays the tail, not the median (Fan Out to 100 and the Component’s Tail Becomes the System’s Median).
- • Availability is the product of all participants and the coordinator. Adding a participant always makes the transaction less available.
- • The coordinator’s log is a coordination point that must be as durable as the data itself; a coordinator with a non-durable log silently invalidates the whole guarantee.
- • Safety holds absolutely: no participant commits unless all voted yes, no matter which messages are lost or which nodes crash.
- • Liveness does not hold: prepared participants may wait without bound for a decision that no live node holds.
- • Committed results are durable at each participant independently once applied — recovery does not need to re-run the transaction, only to learn the decision.
- • Data remains correct at every participant; what degrades is availability of the resources those transactions have locked.
- • Detect: monitor prepared transactions by age. Any entry older than a small multiple of the normal commit time is an incident, not a curiosity.
- • Contain: cap the number of concurrent prepared transactions and the maximum age, so a coordinator failure degrades throughput rather than exhausting the participant.
- • Recover: on restart, a participant scans its log for prepared transactions and asks the coordinator for each decision; a recovered coordinator replays its log and re-broadcasts.
- • Reconcile: where the coordinator’s log is genuinely lost, an operator must decide manually — a *heuristic* decision — and that decision can disagree with another participant’s, which is the one way 2PC can break atomicity.
- • Verify: after any coordinator incident, compare participant states for every transaction in the affected window rather than assuming recovery was clean.
- • Number and maximum age of prepared transactions per participant — the single most important 2PC metric and the one least often collected.
- • Vote latency distribution per participant, so the participant that is about to start causing abort storms is visible before it does.
- • Abort rate split by cause: participant NO, vote timeout, coordinator decision. These have completely different fixes.
- • Lock wait time on participants during the prepare-to-commit window, which is where contention appears first.
- • Coordinator log flush latency — it sits on the critical path of every transaction.
- • Multi-shard writes inside one database you operate, where the shards are homogeneous and the coordinator is part of the same system.
- • Transactions that must be atomic and are short — moving money between two accounts in two shards, where the alternative is an inventory of compensations for a millisecond-scale operation.
- • Where a replicated coordinator is available, so the blocking window is bounded by leader election rather than by a human.
- • When the participants are a database and a message broker in the same data centre and you need the send and the write to be atomic — though an outbox is usually simpler (Atomicity Stops at the Process Boundary).
- • Across organisational boundaries, where you cannot restart a stuck participant or read its logs.
- • With long transactions — anything spanning a user’s think time turns the lock window into minutes.
- • Across regions, where the prepare round trip alone costs tens or hundreds of milliseconds and holds locks for all of it (Three Ways to Accept a Write in More Than One Place).
- • When a participant does not really implement prepare and fakes it — the guarantee evaporates while the operational cost remains.
- • On hot rows, where the extended lock window turns a throughput problem into an outage.
- • A saga with compensating actions: no locks, no blocking, at the price of visible intermediate state (Sagas: Trading Isolation for Availability).
- • Collapse the participants into one store so the commit is local — the cheapest fix whenever the data really has one owner.
- • Transactional outbox plus at-least-once delivery, when the two participants are "my database" and "the rest of the world".
- • A consensus-replicated coordinator (Paxos Commit), which removes the blocking window at the cost of another round trip and a lot of machinery.
- • Optimistic execution with detection and repair: do both writes independently, reconcile continuously, and accept a bounded window of disagreement (Reconciliation Is a Component, Not a Cleanup Script).
PREPARE, vote, decide — and the window where nobody may move
What people believe, and what is true
2PC is a consensus protocol.
It requires unanimity and a live coordinator, so it tolerates no failures at the decision point. Consensus tolerates a minority failing. Paxos Commit is what you get by running 2PC’s decision through consensus.
2PC is always the wrong choice.
Every sharded database that supports cross-shard transactions runs something very close to it, successfully, millions of times a second. The objection is to using it across trust domains with long transactions, not to the protocol.
Three-phase commit fixes the blocking problem.
3PC removes blocking only under a synchronous model with reliable failure detection. In an asynchronous network with partitions — the one you have — it can produce inconsistent decisions, which is worse than blocking.
If a participant times out waiting for the decision, it should just abort.
Then it may abort a transaction the coordinator committed and another participant already applied. The prepared state exists specifically so that unilateral abort is forbidden.
2PC gives you isolation as well as atomicity.
It coordinates commit only. Whatever isolation you get comes from the participants’ own concurrency control, held for longer.
Go deeper
Only the levels this lesson can honestly fill — a missing level is a claim nobody had.
Overview
Ask everyone "can you commit?", wait for unanimous yes, then tell everyone to commit. A yes is a binding promise the participant must be able to keep across a crash.
Practical
If you run 2PC, monitor prepared transactions by age and count on every participant, split abort causes by reason, and set a hard cap on prepared-transaction age. Keep transactions short and keep participants in one data centre.
Advanced
The commit point is the coordinator’s log write, not the broadcast. Presumed abort avoids logging aborts by treating "no record" as abort; presumed commit inverts this and is rarely worth it. The protocol is safe under any message loss and any crash, and live only when the coordinator is up — safety and liveness are cleanly separable here, which is why the objection is always about liveness.
Internals
XA specifies the participant interface: xa_start, xa_end, xa_prepare, xa_commit, xa_rollback, xa_recover. Recovery is driven by xa_recover returning the list of in-doubt XIDs a resource manager holds, which the transaction manager reconciles against its own log. PostgreSQL implements this as PREPARE TRANSACTION, storing the state in pg_twophase and replaying it on startup; the prepared xact keeps its XID in the snapshot horizon, which is why it blocks vacuum. Spanner runs 2PC where each participant is a Paxos group, so participant failure is masked by replication and only the coordinator group’s availability matters — and that group is itself replicated, so the classic blocking window closes.
Apply it
- 🔧 Run a 2PC across two Postgres instances using PREPARE TRANSACTION. Kill the coordinating process between the two prepares and the commits, then find and resolve the in-doubt transactions by hand.
- 🔧 Measure the throughput of a contended row with and without a second 2PC participant, and explain the difference in terms of lock hold time.
- ⚡ A team wants to use XA to keep an order database and a payment provider in sync. The provider offers a REST API. Explain what is wrong with the plan.
- ⚡ Your multi-shard database’s p99 commit latency doubled after a single shard was moved to a different availability zone. Explain the mechanism.
- 💬 Walk me through 2PC. Now tell me exactly which log write is the commit point and why the order matters.
- 💬 Why is 2PC not a consensus protocol?
- 💬 A participant has been in the prepared state for three hours. What is happening on that machine, and what are your options?
- 💬 Does 2PC give you isolation across participants? Under what conditions?