What is two-phase commit, and why do most distributed systems avoid it at scale?
Two-phase commit buys real atomicity across services by making every participant promise to commit before anyone does — and that promise is exactly what can freeze the system solid.
LEARN BY DOINGPut your intuition to work3 steps
What is two-phase commit, and why do most distributed systems avoid it at scale?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#The Protocol That Guarantees Atomicity by Refusing to Move Until Everyone Agrees
Two-phase commit (2PC) solves a narrower, harder problem than a saga: true atomicity across multiple resource managers — either every participant commits, or every participant aborts, with no window where an observer could see a mixed outcome. It's the protocol underneath distributed relational transactions (XA transactions in JDBC/JTA, for instance), and understanding exactly where it blocks is what explains why almost nobody reaches for it in an internet-scale microservices architecture.
#Phase 1: Prepare (voting)
A coordinator sends a PREPARE message to every participant. Each participant does everything short of making the change visible: it writes the transaction's redo/undo information to durable storage and acquires every lock it will need. It then replies YES (I can guarantee this will commit no matter what) or NO (abort, I can't do this). Voting YES is a binding promise, not a status update — from that instant, the participant may not unilaterally abort or commit; it must sit and wait for the coordinator's decision, holding its locks the entire time.
#Phase 2: Commit (or abort)
If every participant voted YES, the coordinator durably records the decision ("commit") and sends COMMIT to everyone; each participant applies the change, releases its locks, and acknowledges. If any participant voted NO — or didn't respond within a timeout — the coordinator sends ABORT instead, and every participant discards its prepared changes and releases its locks.
#Exactly where the blocking happens
The failure mode that gives 2PC its reputation is the gap highlighted above: once a participant votes YES, it has surrendered its right to decide unilaterally. If the coordinator crashes (or the network partitions) after collecting votes but before the final COMMIT/ABORT is delivered, every participant is left in an uncertain, in-doubt state — it cannot commit (the coordinator might have decided to abort based on another participant's NO) and it cannot abort (the coordinator might have already decided to commit, and other participants might already have applied it). The only fully safe move is to keep holding every lock acquired in Phase 1 and wait — potentially forever — for the coordinator to recover and tell it the answer. Every other transaction that needs those same rows blocks too. This is why 2PC is formally described as a blocking protocol: a single node failure can freeze part of the system indefinitely, which is precisely the failure mode a highly-available system is built to avoid.
#The rest of the bill
Even when nothing crashes, 2PC is expensive by construction:
- Latency. Every transaction pays for at least two full network round trips to every participant before it can release a single lock — across availability zones or regions, this is brutal for throughput.
- Lock duration. Locks are held from
PREPAREthrough the finalACK, not just for the duration of local work — under contention this serializes transactions that a saga-based design would let run concurrently. - Availability coupling. Every participant must be reachable for the transaction to complete at all; 2PC picks consistency over availability during a partition, which is rarely the right trade for a public-facing system (this is a direct CAP-theorem consequence: 2PC on a partition either blocks or violates atomicity, never both stays available and consistent).
- Coordinator as a single point of failure. The coordinator's own crash-recovery log becomes critical infrastructure — lose it, and in-doubt transactions can never be resolved automatically.
Three-phase commit (3PC) attempts to shrink the blocking window by adding a "pre-commit" acknowledgment step so participants can safely time out and abort in more scenarios, but it still assumes no network partitions to guarantee non-blocking behavior, which is exactly the assumption that fails in practice — so 3PC sees little real-world use.
#Why (and where) systems avoid it
At internet scale, the standard answer is to give up strict atomicity and adopt the saga pattern: each service commits its own local transaction independently and publishes what happened, with failures handled by compensating actions rather than a shared lock held across the network (see the companion saga lesson for the full mechanics). Where a team still needs an atomic-looking effect across two resources it does control end-to-end — most commonly "write to my database and publish an event" — the modern answer is usually the transactional outbox pattern: write the event into an outbox table in the same local ACID transaction as the business change, then have a separate relay publish it, which sidesteps needing 2PC between the database and the broker entirely. Genuine 2PC survives mainly inside tightly-coupled enterprise middleware (XA-compliant resource managers within a single datacenter, a handful of participants, low-latency network) where the blocking window is short and rare enough to tolerate.
:::danger The in-doubt window is the whole problem 2PC isn't dangerous because it's slow — it's dangerous because a coordinator crash at exactly the wrong instant leaves participants holding locks with no safe unilateral move, for an unbounded amount of time. Any capacity planning that assumes 2PC transactions "usually resolve fast" is planning around the common case and ignoring the failure mode the protocol exists to handle. :::
:::note This is a CAP theorem trade-off wearing a protocol's clothes During a network partition, 2PC either blocks (favoring consistency over availability) or, if a participant unilaterally decides without hearing from the coordinator, risks two participants reaching different decisions (favoring availability over consistency, and breaking the atomicity 2PC exists to provide). There is no third option inside the protocol itself. :::
:::tip When you actually need cross-service atomicity, reach for the outbox pattern first "Update my database and publish an event" doesn't need 2PC between a database and a broker — write the event to an outbox row in the same local transaction as the business write, and let a separate poller/CDC process relay it at-least-once. It gets you the atomicity you actually need without a distributed blocking protocol. :::