Distributed Transactions and Two-Phase Commit
Imagine a bank transfer that must debit an account on a server in Tokyo. It must also credit an account on a server in London. Either both operations happen, or neither does. The bank cannot afford a world where money vanishes or is created from nothing. This is the atomic commitment problem. A set of independent machines, each holding a piece of a transaction, must reach unanimous agreement. They must decide whether to make their changes permanent or discard them. This holds even though any of them might crash or lose network contact at the worst possible moment. Real distributed systems constantly encounter this problem. A distributed database sharded across nodes needs multi-shard transactions to look atomic. A microservice architecture processing an order might touch an inventory service, a payment service, and a shipping service. All of these must agree on the outcome. Message queues coordinating with databases need "exactly-once" semantics. These are really atomic commits in disguise. Without a principled protocol, these systems drift into inconsistent states. Such states are notoriously hard to detect and repair after the fact. The classical answer is the Two-Phase Commit protocol (2PC), introduced in the 1970s. It remains the conceptual backbone of distributed transaction managers. It also underpins XA-compliant databases and many cloud storage engines. 2PC extends the ACID guarantees you expect from a single-node database — Atomicity, Consistency, Isolation, Durability — across a network of participants. It uses a coordinator that collects votes. That coordinator then dictates a single, binding outcome to everyone. This article builds 2PC from first principles. It explains why atomicity across machines is fundamentally harder than atomicity on one machine. It covers the exact message flow and state machine of the protocol. It offers a rigorous complexity and correctness analysis. Critically, it also examines the protocol's famous blocking flaw under coordinator failure. That flaw is precisely the crack that motivates more elaborate consensus machinery. This includes the fault-tolerant reasoning behind the Byzantine Generals Problem.
- Distributed transactions need all participants to agree on commit or abort — a problem called atomic commitment.
- Two-Phase Commit (2PC) splits the decision into a voting phase and a decision phase, coordinated by one node.
- Every participant that votes "yes" enters a prepared state: it must be able to commit or abort no matter what happens next.
- 2PC guarantees safety (no two nodes disagree) but is blocking: a coordinator crash after votes are collected can freeze prepared participants indefinitely.
- Timeouts, participant-to-participant querying, and coordinator logs (write-ahead logging) mitigate but do not eliminate the blocking window.
- 2PC assumes a mostly reliable, non-Byzantine network; harder fault models require richer protocols, foreshadowing consensus and Byzantine-tolerant agreement.
Two-Phase Commit turns a distributed decision into two synchronized rounds — vote, then decree — so that either every participant commits or every participant aborts, at the cost of blocking if the coordinator fails between them.
The idea
A distributed transaction is a transaction whose effects span multiple independent nodes — for instance, separate database shards, separate microservices, or separate storage replicas. Each node performs its local piece of the transaction (a branch), and the whole transaction is correct only if ACID properties hold globally: either all branches commit, or all branches abort, with no partial outcomes visible to any observer. This all-or-nothing requirement is called atomic commitment, and it is strictly harder than single-node atomicity because a single-node system controls its own clock, its own disk, and never has to ask permission from a remote machine that might be unreachable. The core obstacle is that nodes can independently fail, messages can be delayed or lost, and there is no shared memory to consult for a "true" global state — the same fundamental limitation explored in the study of Global State and Distributed Snapshots. A node cannot simply commit its own branch as soon as it is ready, because another node's branch might later fail; it must wait for confirmation that everyone is ready. This creates a chicken-and-egg problem: nodes must synchronize a decision without a central authority that is always available, which is exactly the same tension formalized by the CAP Theorem between consistency and availability during a partition.
Two-Phase Commit solves this by introducing a distinguished coordinator (also called the transaction manager) and a set of participants (resource managers), which typically communicate via Remote Procedure Calls. The protocol proceeds in two rounds: a voting phase, where the coordinator asks every participant whether it can commit, and a commit phase, where the coordinator broadcasts the final, binding decision. The key invariant that makes this safe is the prepared state: once a participant votes "yes," it has made an irrevocable promise — it must be able to commit later, regardless of crashes, until it hears otherwise from the coordinator. This promise is what lets the coordinator make a unilateral, unanimous decision, but it is also exactly what causes the protocol's central weakness: a participant stuck in the prepared state, unable to reach the coordinator, cannot safely proceed on its own.
How it works
Phase 1 — Voting (the "can you commit?" round). The coordinator sends a `PREPARE` (also called `VOTE-REQUEST`) message to every participant. Each participant executes its part of the transaction up to the point of commit, writes a durable log record recording its intended outcome, and replies `VOTE-COMMIT` if it is able to guarantee a future commit, or `VOTE-ABORT` if it cannot (due to a constraint violation, deadlock, or local failure). A participant that votes `VOTE-COMMIT` is now prepared: its log entry survives crashes, and it holds all necessary locks/resources so that it can commit later on command, even if it forgets everything in volatile memory in between.
Phase 2 — Decision (the "here's what happens" round). The coordinator collects all votes.
- If every participant voted `VOTE-COMMIT`, the coordinator writes a `COMMIT` decision to its own durable log (this is the transaction's true commit point), then sends `GLOBAL-COMMIT` to all participants.
- If any participant voted `VOTE-ABORT` (or timed out), the coordinator writes `ABORT` to its log and sends `GLOBAL-ABORT` to all participants.
Each participant, upon receiving the final decision, commits or aborts its local branch, releases its locks, and sends an `ACK` back to the coordinator. Once the coordinator has all acknowledgments, it can safely discard its log record for that transaction. ```pseudocode // ---------- COORDINATOR ---------- function Coordinator_Run(transaction T, participants P[1..n]): // Phase 1: Voting for each p in P: send PREPARE(T) to p votes = {} for each p in P: v = receive_with_timeout(p, TIMEOUT) if v == TIMEOUT or v == VOTE-ABORT: votes[p] = VOTE-ABORT else: votes[p] = v // VOTE-COMMIT // Phase 2: Decision if all votes are VOTE-COMMIT: write_log(T, "COMMIT") // durable: the commit point decision = GLOBAL-COMMIT else: write_log(T, "ABORT") decision = GLOBAL-ABORT for each p in P: send decision to p acks = {} for each p in P: acks[p] = receive_with_retry(p, decision) // resend until ACKed write_log(T, "COMPLETE") // safe to garbage-collect log return decision
// ---------- PARTICIPANT ---------- function Participant_Handle(transaction T): on receive PREPARE(T): if can_commit_locally(T): write_log(T, "PREPARED") // durable, before replying reply VOTE-COMMIT state[T] = PREPARED else: write_log(T, "ABORT") reply VOTE-ABORT state[T] = ABORTED release_resources(T) on receive GLOBAL-COMMIT(T): commit_locally(T) write_log(T, "COMMITTED") release_resources(T) reply ACK on receive GLOBAL-ABORT(T): rollback_locally(T) write_log(T, "ABORTED") release_resources(T) reply ACK on timeout while state[T] == PREPARED: // THE BLOCKING WINDOW: // cannot unilaterally decide commit or abort — // must query other participants or coordinator's backup, // else wait until coordinator recovers query_other_participants_or_block(T) ``` The state machine each participant walks through is small and deliberate — it is exactly this small state space that makes correctness arguments tractable.
Complexity & correctness
Message complexity. For participants, the fault-free execution of 2PC uses exactly messages in each direction pair: `PREPARE` messages out, votes back, decision messages out, acknowledgments back — a total of messages, or . This is optimal up to constant factors: any protocol that reaches a unanimous decision must, at minimum, let the coordinator hear from every participant once and inform every participant once, giving an unavoidable lower bound.
Latency. The protocol takes exactly two sequential round trips in the failure-free case: one for voting, one for deciding. This is the price of the "ask, then tell" structure; it cannot be collapsed to one round trip because the coordinator cannot announce the final decision before knowing every vote.
Durability / log writes. Each participant performs at least one synchronous, durable log write (`PREPARED`, before replying `VOTE-COMMIT`) and one more on receiving the final decision. The coordinator performs one durable write for the decision itself. These forced writes are what make the protocol survive individual node crashes (as opposed to only network delays) — a crashed and rebooted participant can consult its log to know it must honor a previously made promise.
Space. Each node needs persistent state per in-flight transaction (its current phase and vote/decision), plus transient state at the coordinator to track outstanding votes — so overall per transaction system-wide, which is unavoidable since the coordinator inherently must remember something about each participant.
Correctness — the safety invariant. The central claim is: > No two participants reach different final outcomes for the same transaction. Proof sketch. The coordinator's decision is made exactly once and written durably before any `GLOBAL-COMMIT`/`GLOBAL-ABORT` message is sent. Because the coordinator only decides `COMMIT` when it has received `VOTE-COMMIT` from every participant, and every participant that has voted `VOTE-COMMIT` is contractually bound (by having written a durable `PREPARED` record) to honor whatever final decision it receives, the coordinator's single decision is the only source of truth. Even if messages are delayed, duplicated, or a participant crashes and recovers, that participant will consult its log, see it is `PREPARED`, and (upon reconnecting) ask the coordinator for the actual decision rather than inventing its own. Since the coordinator's log contains exactly one recorded decision per transaction, all participants that eventually apply a decision apply the same one. This gives agreement (safety); it does not, by itself, give termination (liveness) in all failure scenarios — that is the blocking problem below.
The blocking flaw. Suppose all participants vote `VOTE-COMMIT` and enter the `PREPARED` state, but the coordinator crashes before sending any `GLOBAL-COMMIT`/`GLOBAL-ABORT` message (or after sending it to only some participants). A prepared participant cannot safely decide on its own:
- It cannot abort, because the coordinator might have already decided `COMMIT` and told some other participant, which then committed.
- It cannot commit, because the coordinator might have decided `ABORT` (e.g., if one other participant had actually voted no, or crashed).
The only safe move is to wait for the coordinator to recover, or to query surviving participants (some 2PC variants add a "cooperative termination protocol" where participants ask each other if anyone already knows the decision — this shrinks but does not remove the uncertainty window, since if all surviving participants are still `PREPARED`, none of them can resolve the ambiguity either). During this wait, the participant typically still holds locks on the affected data, so other transactions can also stall. This is why 2PC is called a blocking protocol: a single coordinator failure at the wrong instant can suspend a subtransaction and its resource locks indefinitely, trading correctness for potentially unbounded unavailability — exactly the tension formalized abstractly in the CAP Theorem.
Why it's still correct, just not always live. Safety (agreement) holds unconditionally as long as the durability guarantees hold — logs are written before replies are sent, and logs survive crashes. What 2PC sacrifices is guaranteed termination under coordinator failure; it never sacrifices consistency. This safety-over-liveness bias is a deliberate design choice, and it is exactly the gap that later three-phase commit and full consensus protocols (like Paxos-based atomic commit) try to narrow, at the cost of extra rounds or extra assumptions about the failure model.
Worked example
Consider an online order that must debit a wallet balance (Participant W) and decrement inventory stock (Participant S), coordinated by an order service (Coordinator C).
| Step | Coordinator C | Participant W (wallet) | Participant S (stock) |
|---|---|---|---|
| 1 | sends `PREPARE` to W, S | — | — |
| 2 | — | checks balance ≥ price; writes `PREPARED` log; replies `VOTE-COMMIT` | checks stock ≥ 1; writes `PREPARED` log; replies `VOTE-COMMIT` |
| 3 | receives both votes: both `VOTE-COMMIT` | (waiting, locks held) | (waiting, locks held) |
| 4 | writes `COMMIT` to its log (this is the commit point) | — | — |
| 5 | sends `GLOBAL-COMMIT` to W, S | — | — |
| 6 | — | debits balance; writes `COMMITTED`; releases lock; sends `ACK` | decrements stock; writes `COMMITTED`; releases lock; sends `ACK` |
| 7 | receives both ACKs; writes `COMPLETE`, may garbage-collect | done | done |
Now trace the failure case: suppose at step 4, right after C writes `COMMIT` to its log but crashes before sending any `GLOBAL-COMMIT` message.
- W and S are both stuck in `PREPARED`. They have locks held (balance frozen, stock frozen) and cannot decide locally.
- C reboots. It reads its durable log and finds `COMMIT` already recorded for this transaction — its own crash-recovery procedure resumes exactly where it left off: it re-sends `GLOBAL-COMMIT` to W and S (idempotently; participants that already committed simply re-ACK).
- W and S receive `GLOBAL-COMMIT`, apply it (or discover they already did, if they'd somehow retried), and release locks.
This shows the mechanism functioning correctly and the danger point clearly: had C's disk failed permanently at that exact moment (rather than merely rebooting), W and S would remain `PREPARED` forever unless a human operator, an external oracle, or a cooperative-termination sub-protocol intervened — this is the blocking scenario made concrete. Contrast with an abort case: suppose Participant S finds stock is zero at step 2 and replies `VOTE-ABORT`. Then at step 3, C sees not all votes are `VOTE-COMMIT`, so at step 4 it writes `ABORT` instead, and at step 5 sends `GLOBAL-ABORT` to both W and S — W, despite having voted "yes" and being fully prepared to debit the wallet, must roll back and release its lock without ever executing the debit. This is precisely why the `PREPARED` state must be reversible: preparedness is a promise of readiness, not a promise of a specific outcome.
Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Treating `PREPARED` as "half-committed" data visible to readers | Confusing prepare with commit | Breaks isolation — dirty reads of a transaction that might still abort |
| Coordinator doesn't durably log the decision before sending it | Assuming the network delivery itself is the commit point | If coordinator crashes after sending to one participant but before others, recovery cannot tell what was decided |
| Participants release locks/resources before receiving the final decision | Impatience to reduce contention | Violates the promise made when voting `VOTE-COMMIT`; can allow the wrong outcome to become locally visible |
| Assuming a participant timeout during `PREPARED` can be safely treated as abort | Reasonable-sounding heuristic borrowed from client-server timeouts | Unsafe: the coordinator may have already told other participants to commit |
| No idempotency on `GLOBAL-COMMIT`/`GLOBAL-ABORT` retransmission | Forgetting messages can be duplicated after a partition heals | Re-applying a commit or abort twice can corrupt state if handlers aren't idempotent |
| Ignoring the coordinator as a single point of failure in system design | Focusing only on participant fault-tolerance | Coordinator crash during the decision window blocks all participants, defeating the purpose of "fault tolerance" |
Pioneers & history
Two-Phase Commit was formalized in the distributed database literature of the late 1970s, growing out of work on making transaction processing systems robust to network partitions and node crashes — the same era that produced foundational ideas about replicated logs and distributed consistency.
Origins · 1 minJim Gray and how this was discovered
Jim Gray was a Turing Award–winning computer scientist whose work at IBM and later Tandem Computers and Microsoft established the theoretical and practical foundations of transaction processing. He is widely credited with formalizing the Two-Phase Commit protocol and the broader notion of the transaction as a unit of reliable computation across unreliable components, work that underlies nearly every commercial database and distributed system built since.
Where this matters
2PC and its descendants appear throughout production infrastructure. Relational databases supporting XA transactions (Oracle, PostgreSQL, MySQL with XA) use 2PC to coordinate commits across multiple database instances or across a database and a message queue. Distributed SQL systems and NewSQL databases (Google Spanner, CockroachDB, YugabyteDB) use 2PC-like commit protocols internally to make multi-shard transactions atomic, often layering it on top of a consensus protocol (like Paxos or Raft) per shard to solve exactly the blocking weakness described above — the coordinator itself becomes replicated rather than a single point of failure. Message-oriented middleware (JMS, MSMQ) and application servers implementing the Java Transaction API (JTA) use 2PC to coordinate a database write with a message send, so that "process order, then notify shipping" either both happen or neither does. Cloud storage systems and distributed file systems use atomic commit protocols for multi-object metadata updates. Even outside databases, the conceptual pattern — collect votes, then broadcast a binding decision — recurs in configuration management and orchestration systems that must roll out a change to many machines atomically. Understanding 2PC's blocking limitation is also the natural on-ramp to appreciating why systems facing malicious or arbitrarily faulty nodes need the stronger guarantees studied under the Byzantine Generals Problem.
Ask the tutor: How would you redesign 2PC's coordinator using a consensus protocol like Raft to eliminate its single point of failure?
Why doesn't a single node's ACID transaction generalize trivially to multiple nodes?
A single-node database controls its own log, its own clock, and its own recovery procedure, so it can decide atomicity purely from local information. Across nodes, no single machine can observe the full state of every participant at once — this is the same limitation studied in distributed snapshots, where there is no instantaneous global view. A node might crash after doing its local work but before other nodes finish theirs, and network messages can be delayed indefinitely, so "all commit or all abort" requires an explicit coordination protocol, not just local durability. Two-Phase Commit exists precisely to manufacture that missing global agreement using message rounds and durable logs, at the cost of extra latency and a potential blocking window if the coordinator itself fails.
Is Two-Phase Commit the same thing as distributed consensus (like Paxos or Raft)?
No, though they solve related problems and share vocabulary like "voting" and "decision." Consensus protocols like Paxos or Raft are designed to keep making progress even if some minority of nodes fail permanently, by requiring only a majority quorum to decide. Classic 2PC requires unanimous votes and has a single coordinator whose failure can block the entire protocol — there is no notion of "majority rules." Modern systems often combine the two: they replicate the coordinator's role using a consensus protocol so that if one coordinator instance fails, another can take over with the same log, which removes 2PC's single point of failure while keeping its two-phase vote/decide structure for atomicity across the actual data shards.
What happens if a participant votes VOTE-COMMIT but then crashes before receiving the final decision?
This is exactly the scenario the durable "PREPARED" log write is designed for. When the participant reboots, it scans its transaction log, finds the still-open PREPARED record, and recognizes it made a binding promise it must honor. It cannot decide the outcome itself, so it contacts the coordinator (or other participants, in cooperative-termination variants) to learn the actual decision, then applies commit or abort accordingly and finally releases its resources. Until it learns the decision, the transaction's locks typically remain held, which is the practical, felt cost of the blocking property — other operations waiting on that data must also wait.
Why must the participant write its PREPARED log record before replying VOTE-COMMIT, not after?
If the participant replied VOTE-COMMIT first and crashed before writing the log, it would lose the record of its promise on recovery. Meanwhile, the coordinator — having received the vote — might proceed to record a global COMMIT decision and tell other participants to commit, unaware that this participant has amnesia about ever voting. On reboot, that participant would have no record of being prepared and might make an arbitrary, possibly wrong, local decision. Writing the log entry before sending the vote guarantees that "I voted yes" and "I am durably ready to honor that" are the same fact, never separated by a crash window.
Does Two-Phase Commit tolerate network partitions safely?
It tolerates them safely from a correctness standpoint — no two participants will ever disagree on the final outcome — but not from an availability standpoint. If a partition separates the coordinator from some prepared participants, those participants simply wait, holding their locks, until the partition heals or a human intervenes; they do not silently proceed and risk disagreement. This favors consistency over availability during a partition, matching the tradeoff described by the CAP theorem: 2PC is a CP-style protocol, sacrificing liveness rather than risking an unsafe decision.
What is Three-Phase Commit and does it actually fix the blocking problem?
Three-Phase Commit (3PC) inserts an extra "pre-commit" round between voting and the final decision, so that participants learn the decision is coming before it is finalized, which lets a newly elected backup coordinator infer the right outcome in more failure scenarios. Under a synchronous network model with bounded message delays and a reliable failure detector, 3PC can indeed avoid blocking in many cases. In real, asynchronous networks — where messages can be arbitrarily delayed and you cannot reliably distinguish a crashed node from a slow one — 3PC does not fully solve blocking either, and it adds an extra round of messages and latency, so it is rarely used in practice compared to 2PC combined with a replicated, highly available coordinator.
How do real systems avoid 2PC's blocking problem in production?
Rather than trying to make a single coordinator immune to failure, production systems typically replicate the coordinator's state using a consensus protocol, so a majority of coordinator replicas can elect a new leader and resume exactly where the old one left off, using the durable decision log. Some systems instead avoid multi-participant atomic commit entirely by restructuring the workload — using sagas (a sequence of local transactions with compensating actions) or idempotent, retryable operations instead of strict cross-node atomicity. These approaches trade strict atomicity for eventual consistency and explicit compensation logic, accepting temporary inconsistency in exchange for guaranteed forward progress even under coordinator failures.