An order is written to one database and the payment to another. Both must happen, or neither. Each database can commit its own part atomically, but nothing local stops one from committing while the other crashes. Two-phase commit (2PC) is the classic protocol for this problem, built into most relational databases through the XA standard and used inside distributed SQL databases today.
It also has a famous weakness: if the coordinator fails at the wrong moment, participants can be stuck holding locks with no way to decide on their own. This post walks through the protocol with a sequence diagram, every failure case, how real systems implement it, and when a saga or an outbox is the better tool.
The roles
- Coordinator, or transaction manager: drives the protocol and decides the outcome. It can be a library inside your application server, a separate service, or a component of a distributed database.
- Participants, or resource managers: the databases or queues that each hold part of the transaction.
Every party keeps a durable log. The log writes are not an implementation detail; they are what makes the protocol survive crashes.
The protocol
Phase 1: prepare.
- The application does its work in each participant inside one global transaction, then asks the coordinator to commit.
- The coordinator sends PREPARE to every participant.
- Each participant makes sure it can commit no matter what happens next: it writes the transaction's changes and a prepare record to its durable log, keeps its locks, and votes YES. If it cannot commit, for example because a constraint failed, it votes NO and rolls back.
Phase 2: commit or abort.
- If every vote is YES, the coordinator writes a COMMIT decision to its own log. That write is the commit point: from here on, the transaction is committed, even if every machine crashes a millisecond later.
- The coordinator sends COMMIT to every participant. Each one commits, releases its locks and acknowledges.
- If any participant votes NO or does not answer in time, the coordinator decides ABORT and sends ROLLBACK instead.
A YES vote is a promise. After voting YES, a participant has given up the right to decide alone: it must commit if told to commit, even after a restart, and it cannot roll back on its own because the coordinator may already have decided to commit.
The cost
For N participants, a successful commit takes two round trips from the coordinator and four messages per participant: PREPARE, vote, COMMIT and acknowledgement. Each participant forces its log to disk twice, at prepare and at commit, and the coordinator forces its decision once. Locks are held for the whole time, so the slowest participant decides how long every other participant blocks conflicting work.
What happens when things fail
| Failure | When | Outcome |
|---|---|---|
| Participant crashes or times out | Before voting | Coordinator aborts the transaction |
| Participant votes NO | Phase 1 | Coordinator aborts the transaction |
| Participant crashes | After voting YES | On restart it finds the prepare record and asks the coordinator for the outcome, holding its locks meanwhile |
| Coordinator crashes | Before writing the decision | Prepared participants wait. On recovery, with no decision in its log, the coordinator aborts |
| Coordinator crashes | After writing COMMIT | On recovery it reads its log and resends COMMIT |
| Network partition | After a participant voted YES | That participant blocks until it can reach the coordinator |
The rows that matter are the ones where a participant has voted YES and cannot hear the decision. It cannot commit, because another participant might have voted NO. It cannot abort, because the coordinator might have committed. It waits, with its locks held, for as long as the coordinator is unreachable. Every other transaction that touches those rows waits too. This is what people mean when they call 2PC a blocking protocol.
Stuck prepared transactions in practice
PostgreSQL implements the participant side with PREPARE TRANSACTION, COMMIT PREPARED and ROLLBACK PREPARED. The feature is disabled by default (max_prepared_transactions is 0), and the documentation warns that it is meant for external transaction managers, not applications. A forgotten prepared transaction keeps its locks and stops VACUUM from reclaiming storage.
Finding and resolving one looks like this:
-- Prepared transactions waiting for a decision
SELECT gid, prepared, owner, database
FROM pg_prepared_xacts
ORDER BY prepared;
-- Only after checking the coordinator's log for the real outcome:
COMMIT PREPARED 'order-7f3a';
-- or
ROLLBACK PREPARED 'order-7f3a';
Never resolve a prepared transaction by guessing. If the coordinator committed and you roll back one participant, you have created exactly the inconsistency 2PC exists to prevent. MySQL offers the same mechanism with XA PREPARE, XA COMMIT and XA RECOVER.
Variations that reduce the pain
- Presumed abort. If the coordinator has no record of a transaction, the answer is abort. This saves log writes for aborted transactions and makes recovery simpler; most implementations use it.
- Read-only participants. A participant that changed nothing can vote "read-only" and skip phase 2.
- Three-phase commit. Adds a pre-commit round so participants can finish without the coordinator, but only if message delays are bounded and the network never partitions. Those assumptions rarely hold, so 3PC is mostly a textbook topic.
- Replicate the coordinator and participants. The real fix for blocking is to make each role a replicated group that survives the loss of a machine. Google's Spanner runs two-phase commit over Paxos groups for exactly this reason, and other distributed SQL databases use the same idea with Raft. Two-phase commit still decides the outcome; consensus keeps each party available.
When to use 2PC, and when not to
2PC fits when all participants are databases or brokers that support a prepare step, you control all of them, transactions are short, and you need atomicity and isolation across them. It works poorly across services owned by other teams, across HTTP APIs that have no prepare step (a payment provider will not hold your charge in a prepared state), and for business processes that last minutes or days.
| Two-phase commit | Saga | Transactional outbox | |
|---|---|---|---|
| Consistency | Atomic; locks give isolation | Eventual, with compensating steps | Eventual between a database and a broker |
| Blocking | Yes, while a decision is pending | No | No |
| Requires | Participants that support prepare | Compensation logic for each step | One database plus a relay |
| Typical use | Databases or shards you run | Business workflows across services | Publishing events reliably after a write |
The saga pattern diagram shows the compensating alternative, and the transactional outbox pattern covers the most common case teams reach for 2PC: writing to a database and publishing a message together. Often the best fix is structural: put the data that must change together in one database, or one shard, as the database sharding diagram discusses.
The two-phase commit diagram
Diagram contents
- Application
- Coordinator
- Orders DB
- Payments DB
- phase 1: prepare
- phase 2: all votes YES
- commit()
- PREPARE
- YES (prepare record on disk)
- COMMIT
- ACK, locks released
- ACK
- ROLLBACK
- committed or aborted
How to read it:
- Four lifelines. The application, the coordinator, the orders database and the payments database. Time runs downward.
- The par fragment is phase 1. PREPARE goes to both participants at the same time, and each returns a vote after writing its prepare record.
- The alt fragment is phase 2. The upper branch is the normal case: all votes are YES, the coordinator logs COMMIT and both participants commit and acknowledge. The lower branch is the abort path.
- The note marks the in-doubt window between a YES vote and the decision, when participants hold locks and cannot decide alone.
- The Markdown block lists the failure cases from the table above.
Drawing distributed transactions honestly
- Show the log writes. A 2PC diagram without "write prepare record" and "write decision" leaves out the part that makes it correct.
- Mark the commit point. One message or log write decides the outcome; point at it.
- Draw the window where things block. It is the reason teams avoid 2PC, so a diagram that hides it misleads.
- Put failure paths in their own diagrams. A coordinator crash after prepare is worth its own sequence with the recovery steps.
For the notation itself, the sequence diagram example covers lifelines, fragments and naming.
Build it in Archboard
- Sign in with Google or GitHub and create a diagram.
- Add four Lifeline nodes from the Sequence pack: Application, Coordinator, Orders DB and Payments DB.
- Draw the messages as edges, top to bottom: commit request, PREPARE to each database, the YES votes, COMMIT to each database and the acknowledgements.
- Add a Fragment from the Sequence pack with the operator
pararound the prepare messages, and a second Fragment with the operatoraltaround the commit and rollback branches. - Add a Note for the in-doubt window and a Markdown block from the Core pack with the failure table.
The example uses 7 diagram nodes, within the Free plan.
FAQ
Why is two-phase commit called a blocking protocol? A participant that has voted YES cannot commit or abort on its own. If the coordinator becomes unreachable before sending the decision, the participant waits with its locks held, and so does every transaction that needs those rows.
Is two-phase commit the same as a distributed transaction? It is the most common way to commit one. A distributed transaction spans several resources; two-phase commit is the protocol that makes its commit atomic.
Does three-phase commit solve the blocking problem? Only under assumptions that rarely hold: bounded message delays and no network partitions. Replicating the coordinator and participants with consensus, as Spanner does, is the practical fix.
Can I use two-phase commit across microservices? Rarely well. Services would need to expose a prepare step and hold locks for each other, which couples their availability. Sagas and the outbox pattern are the usual choices between services.
Does two-phase commit guarantee isolation? It guarantees atomicity. Isolation comes from the participants' locks, which 2PC keeps until the decision arrives, so concurrent transactions do not see partial results.