Distributed Transactions: Two-Phase Commit and Its Alternatives
Distributed transactions explained: two-phase commit step by step, the 2pc coordinator failure that leaves participants in doubt, consensus-backed coordinators, distributed transaction examples from XA to Spanner and CockroachDB, and when to pick something else.
Sharding buys write throughput by splitting a dataset across machines, and the bill arrives the first time one logical change spans two of them: debit one shard, credit another, crash between the two, and the books no longer balance. That is the seam database sharding opened and sharding vs replication exposed; atomicity, which a single database hands out for the price of one write-ahead-log fsync, becomes a distributed problem the moment the rows involved stop sharing a node.
The primitive that solves it is old (two-phase commit dates to the 1970s database literature) and its failure modes are old with it, which is why every modern distributed database rebuilt it on a newer foundation. The problem statement lives in distributed consensus: machines that crash independently, on a network that loses messages, with no shared clock, the environment every distributed system imposes. This article covers the protocol, the blocking problem that has followed it for decades, the consensus-backed fixes, and the alternatives a design should weigh first.
A distributed transaction is a transaction whose effects span multiple nodes (shards, databases, services) yet must still behave like a single transaction: all effects commit, or none do. The hard part is not issuing the writes. It is making “all or nothing” true across machines that fail independently, when any of them can disappear mid-protocol.
Why atomicity stops being free
On a single node, atomicity is nearly an accounting trick: the write-ahead log turns a multi-page write into one decision, and the commit record makes that decision durable with a single fsync. Crash before the record, the transaction never happened. Crash after it, the recovery pass finishes the job. Either way: one machine, one log, one truth, and it is that same log database replication ships to followers to keep copies in sync.
Split the transaction across nodes and the trick stops working. Debit runs on shard A, credit runs on shard B, the crash lands between them, and there is no single log in which a commit record could have been written, nor any recovery pass that sees both halves. A and B cannot consult their local logs to learn what the other decided, because neither decided anything yet. Worse, A is holding locks while it waits to find out. The only way out is to add a node whose job is to decide for both (a coordinator) plus a protocol in which every participant promises in advance to obey that decision. That protocol is two-phase commit.
Two-phase commit
Two-phase commit (2PC) runs one transaction across independent participants under a single coordinator. Phase 1 (voting) asks everyone whether they can commit. Phase 2 (the decision) tells everyone whether they will. The steps:
- The coordinator sends a PREPARE message for the transaction to every participant.
- Each participant does the work up to the line (writes the data and its undo information durably, acquires the locks it will hold through commit) and votes YES. A participant that cannot prepare votes NO and rolls back unilaterally, releasing everything.
- The coordinator gathers the votes. If they are all YES, it writes COMMIT to its own durable decision log; otherwise it writes ABORT.
- The coordinator sends the decision to every participant.
- Each participant applies the decision, releases its locks, and acknowledges. The coordinator retries this step for participants that were down, because the decision is now immutable fact; it cannot and will not change.
Read 2PC as a stack of promises and it makes sense. A YES vote is a signed contract: the participant has made its half durable and promises to accept either decision. The coordinator’s log entry is the moment of no return, once written, the outcome cannot change, which is why the coordinator fsyncs before sending phase 2. The acknowledgment step exists because a participant that misses the COMMIT message will otherwise sit holding locks until someone tells it again.
What 2PC guarantees is the important part: every participant reaches the same outcome, and no participant commits unless every participant promised. What it cannot survive is losing the coordinator at the wrong moment, and unlike consensus, it has no majority arithmetic to fall back on. That failure is the next section.
2PC coordinator failure: the blocking problem
Watch the protocol at its most unlucky moment. Every participant has voted YES. Each is holding locks, with undo information on disk and a promise to obey. The coordinator writes COMMIT to its decision log, and dies before sending the message. The cluster now contains participants that are contractually unable to move.
They cannot commit: the decision might have been ABORT; the coordinator’s rules are unknowable from outside, and a participant that commits unilaterally may contradict the outcome recorded on the coordinator’s disk. They cannot abort: the decision might already be COMMIT, and an abort would contradict a transaction that committed elsewhere. So they wait, holding locks, for a coordinator that may be restarting, may be down for an hour, or may never return. This is the in-doubt transaction; the state 2PC is famous for, in which the failure of the one node that holds no data freezes the nodes that hold all of it.
The classic mitigations are operational, not algorithmic. A coordinator that recovers replays its decision log and finishes the protocol, and the in-doubt participants wait it out. A coordinator with a standby, or with its decision log on shared storage, shortens the freeze. And administrators can force an in-doubt transaction to a heuristic completion (committing or aborting it by hand) which releases the locks and risks exactly the inconsistency the transaction existed to prevent. Databases surface this machinery with visible reluctance: MySQL lists prepared XA transactions through XA RECOVER, PostgreSQL exposes them through pg_prepared_xacts, and both manuals are clear that manual resolution is a last resort.
The literature’s algorithmic fix is three-phase commit (Skeen, 1981), which adds a pre-commit round so participants can usually infer the outcome without the coordinator. But 3PC’s argument leans on assumptions about message delivery (bounded delays, no partitions) and when the network partitions anyway, it can still leave two groups resolving toward different outcomes. The industry concluded that making the coordinator itself fail-safe was the better problem to solve. The modern answer does exactly that, with consensus.
Consensus-based commit: making the coordinator fault-tolerant
The fix is not a new commit protocol; it is the old one, run on a coordinator that no longer has a single point of failure. Write the coordinator’s decision log through consensus: each PREPARE, each vote, and the final decision becomes a replicated log entry agreed by a majority before the coordinator acts on it. A coordinator that dies is replaced by another node that reads the same log and finishes the protocol from the last durable fact. There is no uncertain state to reconstruct, because no state ever lived on a single machine.
Production runs two shapes of the pattern. In the first, the transaction manager is a consensus-backed service of its own; a small coordinator fleet whose members replicate the decision log through an etcd- or ZooKeeper-style engine, and the participants never know which member they are talking to. In the second, the database is the consensus system: Spanner runs two-phase commit across shard groups, each of which is already replicated by Paxos, so the coordinator’s decision is durable the moment a majority of one group records it. CockroachDB does the same over Raft ranges; every durable fact of the transaction sits on consensus-replicated storage, and the commit decision is no exception. Kafka’s transactional producer follows the first shape: a broker-side transaction coordinator keeps transaction state on Kafka’s own replicated log, which is how it offers exactly-once delivery across a consume-transform-produce pipeline.
What consensus buys 2PC is worth stating plainly, because it is the promise from the distributed consensus problem statement, cashed in: the coordinator’s decision survives the coordinator. Blocking windows shrink from “until the coordinator recovers” to “until a majority member is reachable,” and the in-doubt transaction stops being an operator’s recurring nightmare and becomes a brief outage. The cost is the cost of consensus everywhere; a majority must be alive for the decision to be written, and the deployment must run one more consensus service, or accept the one the database already provides.
Distributed transaction examples
Where the machinery shows up, from oldest to newest:
- XA across two databases: the classic enterprise case: debit an account in one database, credit it in another, one coordinator, XA RECOVER waiting in the wings if it dies. MySQL and PostgreSQL both speak XA today, with prepared transactions exposed through their recovery catalogs and views.
- Spanner: two-phase commit across shard groups, each group Paxos-replicated across datacenters; the coordinator’s decision is durable because it lives in a quorum, and so is every participant.
- CockroachDB: a SQL layer over a key-value store of Raft ranges; transactions write provisional intents across ranges and a two-phase structure commits them, with every durable fact on consensus-replicated storage.
- Kafka’s transactional producer: consumed and produced messages committed as one unit by a broker-side transaction coordinator whose state rides Kafka’s replicated log, the messaging system’s answer to exactly-once.
- Application-level two-phase across services: reserve stock in one service, charge the card in another, hold, then confirm; hand-rolled 2PC in disguise, and the place where most teams discover why they should have used a saga instead.
That last example is a warning, not a recommendation: every hand-rolled coordinator eventually meets its unlucky moment. The patterns that scale past a handful of services (compensating transactions, the outbox pattern, sagas) are covered in the saga pattern, which also owns the decision of when 2PC should lose to one.
Choosing an approach
Most designs that think they need distributed transactions need them in one specific place, and the first decision is whether that place can be engineered away: route the transaction to one node by key, accept a weaker guarantee, or restructure so the multi-node write is not atomic at all. When the transaction cannot be avoided, the choice is between the families this article has covered:
| Approach | Guarantee | Failure behavior | Latency cost | Operational weight | Best fit |
|---|---|---|---|---|---|
| Route by key | Full ACID, one node | No cross-node risk | None extra | Schema and access design | Transactions that fit one shard |
| Classic 2PC (XA) | Atomic commit across nodes | Blocks on coordinator loss; in-doubt locks | Two round trips plus fsyncs, locks held throughout | Coordinator HA and log management | A handful of databases, modest write rates, teams that accept the toil |
| 3PC | Atomic, non-blocking on paper | Unsafe under partitions; can decide inconsistently | Three round trips | Same as 2PC, with a worse failure mode | Almost nothing; historical interest |
| Consensus-backed 2PC | Atomic commit, decision survives coordinator failure | Blocks only while no majority is reachable | Consensus write on the decision path | A consensus service, or a database with one built in | Distributed SQL databases; systems already running Raft or Paxos |
| Sagas | Atomicity relaxed to eventual, with compensation | No blocking; compensations run on failure | Per-step latency only | Compensation logic and workflow | Long-lived, cross-service work; the full comparison lives in the saga pattern |
Three rules compress the table. First, engineer the transaction away before paying for one: a schema that keeps a transaction on one shard beats any protocol, and sharding by the transaction’s hot key is the cheapest fix there is. Second, when cross-node atomicity is genuinely needed, put the coordinator on consensus; a replicated decision log converts 2PC’s worst failure from an incident into an outage. Third, if the transaction spans services on human timescales, 2PC’s locks are the wrong tool entirely: sagas trade the blocking for compensation logic, and the 2pc-vs-saga decision belongs to the saga pattern, which owns it.
Common mistakes
- Treating a prepared transaction as safe to walk away from. A participant that voted YES is holding locks and a promise: “we’ll clean it up later” is how in-doubt transactions accumulate until one is resolved by hand in the wrong direction.
- Running the coordinator without a durable decision log. A coordinator that recovers without its log must guess, and a guessed decision contradicts whichever participant received the real one.
- Calling 2PC a consensus protocol. It takes everyone’s vote and blocks on one failure; consensus takes a majority’s and survives a minority’s. The comparison is in consensus algorithms, and the distinction is the entire reason this article’s fix works.
- Locks held at human timescales. A prepare-to-commit window measured in minutes turns any transient failure into a fleet-wide freeze. Participant timeouts should be aggressive, and long transactions should not be distributed at all.
- Preparing on asynchronously replicated storage. A YES vote durably recorded on a primary that can fail over to a replica that never saw it is a vote that was never really cast. Prepared state must be durable on a quorum: the database replication argument, applied to the transaction’s own state.
FAQ
Why does 2PC block when the coordinator fails?
Because a participant that voted YES made a binding promise without receiving the outcome. Committing risks contradicting an ABORT decision; aborting risks contradicting a COMMIT. Both moves can violate atomicity, so the only safe act is to wait (holding locks) until the coordinator’s decision log can be read again. The blocking is not an implementation flaw; it is the cost of atomicity with an unprotected decider.
Do I need distributed transactions at all?
Usually less than the design suggests. If the transaction’s rows share a shard key, route it to one node and keep the single-node guarantee. If the work spans services, a carefully designed sequence of local transactions with idempotent, retryable steps often beats coordination outright; the event-driven patterns in message queues and the saga alternative both belong on the shortlist before 2PC does.
What is an in-doubt transaction, and how do I clear one?
It is a transaction that voted YES and never learned the outcome. The supported path is to read the coordinator’s decision log and let it finish the protocol, which is what MySQL’s XA RECOVER and PostgreSQL’s pg_prepared_xacts expose. The unsupported path is a heuristic commit or abort by hand, which releases the locks and risks the exact inconsistency the transaction existed to prevent.
Is three-phase commit the fix for 2PC’s blocking?
It tries, and fails on partitions. 3PC adds a pre-commit round so participants can infer outcomes without the coordinator, but its correctness leans on bounded message delays; an assumption a partition violates, at which point two groups can infer opposite outcomes. The working fix is less exotic: keep the coordinator’s decision in a quorum, so a live node always knows the answer.
How do distributed transactions relate to sagas?
They answer the same question: “how do I make multi-node work all-or-nothing?”, with opposite tools. 2PC holds locks and coordinates a single instant; a saga commits local transactions step by step and undoes them with compensations when a later step fails. The trade-off between them (isolation, failure handling, when each wins) is the saga pattern’s question, and it is answered in the saga pattern.
Related articles
- Next read: the saga pattern, the alternative most systems actually ship: local transactions plus compensation, and the 2pc-vs-saga decision, answered properly.
- database replication; the durability underneath every promise a participant makes.
- database sharding, the design that most often forces a transaction to span nodes in the first place.
- distributed consensus, the primitive that fixes the coordinator: quorums, FLP, and why majorities work.
- the Raft consensus algorithm: the engine under modern transaction coordinators, covered in depth.