Reqflow
← All articles
Deep DiveMarch 21, 2026·9 min read

Two-Phase Commit: Why Distributed Transactions Are Hard and What People Use Instead

2PC is the textbook solution for distributed transactions. It's also why most distributed systems avoid distributed transactions entirely.

distributed systemstransactionsconsistency2pcsagas

Two-phase commit (2PC) is the classical protocol for achieving atomic commits across multiple database nodes or services: either all of them commit the transaction, or none of them do. It's been in textbooks since the 1970s. Every distributed systems engineer encounters it eventually, usually when they're trying to keep two services in sync and wondering why it's so hard. Here's how it works, why it's fragile, and what modern distributed systems use instead.

Phase 1: prepare

In the prepare phase, a coordinator node sends a PREPARE message to every participant (the nodes involved in the transaction). Each participant does the work: writes the data to a write-ahead log, acquires locks on any rows it's about to modify, and replies VOTE-COMMIT if it's ready to commit or VOTE-ABORT if something went wrong. Critically, a VOTE-COMMIT is a durable promise: the participant is saying 'I will commit this if you tell me to, and I will not lose this data even if I crash before hearing back.' The participant must hold its locks and the prepared state until it hears from the coordinator.

Phase 2: commit or abort

If the coordinator received VOTE-COMMIT from all participants, it writes a commit record to its own log and sends COMMIT to all participants. If any participant voted ABORT, the coordinator sends ABORT to all participants. Participants apply the commit or release the prepare state. From the outside, the transaction is atomic: it either committed on all nodes or rolled back on all nodes.

The blocking problem

2PC has a fatal flaw: it blocks if the coordinator crashes during phase 2, after participants have voted COMMIT but before the coordinator has sent the commit decision. Participants are stuck: they hold their locks, they've promised to commit, but they can't proceed because only the coordinator knows whether to commit or abort. They cannot resolve this on their own because they don't know what the other participants voted. This blocking can last until the coordinator recovers, which may be minutes or hours. Held locks mean other transactions in those rows are blocked for the entire duration. In a high-traffic system, this is catastrophic.

Paxos, Raft, and avoiding the problem

Three-phase commit (3PC) partially addresses the blocking problem by adding a pre-commit phase, but it introduces new failure modes and is rarely used. The practical industry answer is to use consensus protocols (Paxos, Raft) at the storage layer, which handle leader crashes internally, or to avoid distributed transactions entirely by redesigning the data model so that a single transaction only touches a single partition (and a single Raft group). Google Spanner uses Paxos groups and TrueTime to achieve global transactions with bounded latency. CockroachDB does the same. For most applications, the right answer is neither.

Sagas: choreographed eventual consistency

The pattern most microservice architectures use instead of 2PC is the Saga pattern: break the multi-step transaction into a sequence of local transactions, each with a compensating transaction that undoes its effect. If step 3 fails, steps 2 and 1 are compensated (rolled back via compensating transactions). Sagas can be orchestrated (a saga orchestrator calls each step and handles failures) or choreographed (each service emits events that trigger the next step). Sagas are not the same as ACID transactions: they're eventually consistent, and compensations may be visible to users briefly. But they eliminate cross-node locks and don't block on coordinator failure, which makes them far more practical for microservice architectures.

Practical guidance

If you need strict atomicity across two services: first ask whether you can redesign the model so the critical write fits in a single service's database (usually yes). If not, and if you're using a database like Spanner/CockroachDB that handles distribution internally, use their native transaction support. If you're in a microservice architecture with heterogeneous databases, design for sagas with compensating transactions and accept that the system is eventually consistent across service boundaries. Only reach for 2PC when you have a genuine need and a coordinator failure won't cause extended lock contention. That's a narrow window.

Explore the concepts

See it in action

← Back to all articles