Two-phase commit (2PC)
Understand how two-phase commit achieves atomicity across multiple databases, why the coordinator is a blocking single point of failure, and what alternatives engineers reach for instead.
The problem
Your e-commerce checkout touches three services: the payment service (charge the card), the inventory service (reserve the items), and the orders service (create the order record). You fire all three calls. The payment service succeeds. The inventory service times out. The orders service never receives a request because your coordinator stopped at the timeout.
The customer's card is charged. No inventory is reserved. No order exists in the system. The customer calls support. You have a data inconsistency that requires a human to resolve.
This happens because each service commits independently. There is no mechanism to say "all three must succeed or all three must roll back." Distributed atomicity is the problem two-phase commit solves.
What it is
Two-phase commit (2PC) is a distributed consensus protocol that ensures all participants in a distributed transaction either all commit their changes or all roll them back. A single coordinating process orchestrates two phases: the Prepare phase ("can you commit?") and the Commit phase ("please commit now"). Named after the two round trips between the coordinator and the participants.
Analogy: Think of a wedding ceremony. The officiant (coordinator) asks each party "do you take this person as your spouse?" before anyone says "I do." Both parties must answer yes while the ceremony is paused. Only when both agree does the officiant declare them married. If either says no or stays silent, the ceremony stops and nothing changes. No vows are exchanged until both parties commit.
Two round trips, one coordinator
2PC adds exactly two extra network round trips to every distributed write: one for PREPARE (collecting votes) and one for COMMIT/ABORT (broadcasting the decision). For a three-participant transaction over a 5ms LAN, that is roughly 10ms of added coordination overhead before any participant can release its locks.
How it works
spawnSync d2 ENOENT
Phase 1 (Prepare): The coordinator sends PREPARE to every participant. Each participant:
- Writes a prepare record to its local write-ahead log so it can recover after a crash.
- Acquires all locks required for the transaction's changes.
- Executes the work but does not yet make it durable.
- Replies YES (ready to commit) or NO (cannot commit due to a constraint violation, lock conflict, or timeout).
Phase 2 (Commit or Abort): If all participants reply YES, the coordinator writes a commit record to its own WAL and sends COMMIT to all participants. If any participant replies NO (or times out), the coordinator sends ABORT. Participants apply the commit or release their prepared state as instructed, then release their locks.
Coordinator pseudocode
function run_distributed_transaction(participants, operations):
txn_id = generate_id()
write_to_log(PREPARE_SENT, txn_id)
# Phase 1: Prepare
votes = []
for each participant in participants:
response = send_prepare(participant, txn_id, operations[participant])
votes.append(response)
if all votes == YES:
write_to_log(COMMIT, txn_id) # coordinator commit point
for each participant in participants:
send_commit(participant, txn_id)
write_to_log(COMMIT_COMPLETE, txn_id)
return SUCCESS
else:
write_to_log(ABORT, txn_id)
for each participant in participants:
send_abort(participant, txn_id)
return ABORTED
The coordinator crash failure
The most dangerous failure mode: the coordinator crashes after sending COMMIT to P1 but before sending to P2 and P3.
P1 has committed. P2 holds its locks and waits. P2 cannot commit on its own because it does not know whether the coordinator's decision was COMMIT for all or just P1. P2 cannot abort because P1 has already committed. P2 is blocked until the coordinator recovers.
The blocking problem
The unsafe window: all YES received, no COMMIT sent yet
The moment every participant votes YES and before any participant receives COMMIT is the 2PC danger zone. A coordinator crash here leaves every participant holding live row locks with no way to proceed. Row-level locks on a high-traffic table can cascade into application-level timeouts within seconds.
This is 2PC's fundamental flaw: participants hold locks from the moment they send YES until they receive a COMMIT or ABORT decision from the coordinator. If the coordinator crashes in this window, all participants are blocked indefinitely. Those locks block every other transaction touching those rows.
The dangerous window is: after all participants vote YES (phase 1 complete) but before all participants acknowledge the decision (phase 2 complete). Any coordinator failure in this window causes a block that only coordinator recovery can resolve.
No participant can break the deadlock unilaterally. If P2 aborts on its own but P1 already committed, the transaction is half-committed, which is worse than a block. The protocol requires all participants to receive the same decision from the coordinator.
In practice, coordinator recovery is fast (typically seconds for a simple restart). But "potentially indefinite block" is the theoretical guarantee, and it makes 2PC unsuitable for long-running transactions or systems where coordinator failures are frequent.
spawnSync d2 ENOENT
Recovery via coordinator log
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.