Guide · System design
Two-Phase Commit and Its Problems: Why 2PC Blocks
24 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- Two-phase commit (2PC) makes several databases commit one transaction together or cancel it together. A transaction is a group of changes that must all happen or none.
- First each database votes. Then one coordinator, the program running the protocol, writes the decision to its disk and announces it. A yes vote is a promise to commit if told.
- If the coordinator crashes after the votes but before announcing, a database that voted yes cannot safely decide alone. It waits and keeps its locks, which stop others changing those rows.
- Three-phase commit (3PC) adds a round so survivors can finish alone. It is safe only if silence always means a crash, so a network split, where machines cannot reach each other, breaks it.
- Beginners can stop after the 3PC sections. Engineers then get tested PostgreSQL and MySQL output for stuck transactions, the clean-up cost, and how Paxos Commit and Google Spanner copy the coordinator onto several machines.
What goes wrong when you move money between two databases?
Money goes missing when one database saves its half of a transfer and the other does not. Each database can promise only its own changes, so a crash in between leaves a mess.
Two-phase commit is the classic fix for this. This guide shows what it fixes, the new problem it brings, and what real systems do about that problem.
Picture a common setup. A bank keeps savings balances in one database. Its wallet, which customers use to pay shops by phone, runs on a second database built later by another team. The setup is made up, but the problem is real.
The first transfer service was the naive version: two separate commits in a row. It took Rs 5,000 off the savings balance and committed. Then it added Rs 5,000 to the wallet and committed.
One night the service crashed between those two steps during a software update. Thirty-seven customers lost money that never reached their wallets (numbers made up). The operations team spent two days matching records by hand.
The team now wants one rule. A transfer happens fully or not at all, even if a machine dies halfway.
What is two-phase commit?
Two-phase commit, or 2PC, is a protocol (a fixed set of message rules) that makes several databases commit one transaction together, or cancel it together. First every database votes, then one coordinator announces the result.
Commit and abort. To commit means to make the changes permanent and visible. To abort, also called rolling back, means to throw the changes away as if they never happened.
Atomic. A change is atomic when it happens completely or not at all, never halfway. One database gives you this for its own data. Across two databases, nobody gives it to you unless you add a protocol.
Coordinator and participants. The coordinator is the program that runs the protocol and makes the final decision. Each participant is a database that does part of the work and votes.
In the bank setup, the coordinator is the transfer service. The two participants are the savings database and the wallet database.
How does two-phase commit work step by step?
Two-phase commit works in two rounds of messages. In the prepare phase, the coordinator asks every participant to get ready and vote. In the commit phase, it tells all of them the one final decision.
Here is one Rs 5,000 transfer under 2PC. Follow the arrows from the customer's tap.
One transfer under two-phase commit
Drag to explorePhase one collects the votes. Notice the log box: the decision is saved to disk before any commit message leaves.
Diagram connections:
- Customer taps Move Rs 5,000 to Prepare the debit
- Prepare the debit to Prepare the credit
- Prepare the credit to Did both vote yes?
- Did both vote yes? to Write COMMIT to its log (Yes)
- Did both vote yes? to Send abort to both (No, or no reply in time)
- Write COMMIT to its log to Send commit to both
- Customer taps Move Rs 5,000: App → Transfer coordinator
- Prepare the debit: Coordinator → Savings database
- Prepare the credit: Coordinator → Wallet database
- Did both vote yes?: Both votes → Coordinator
- Write COMMIT to its log: Coordinator → Its own disk
- Send commit to both: Coordinator → Both databases
- Send abort to both: Coordinator → Both databases
Do the work, but do not commit
The coordinator asks the savings database to debit Rs 5,000 and the wallet to credit Rs 5,000. Each makes its change inside an open transaction and locks the rows it changed.
Prepare and vote
The coordinator sends prepare to both. Each participant saves its changes to disk so they survive a crash, then votes yes. If anything is wrong, such as a low balance, it votes no.
Decide
If both votes are yes, the decision is commit. If any vote is no, or a vote does not arrive in time, the decision is abort.
Write the decision down
The coordinator writes the decision to its own log on disk before telling anyone. That record is now the single source of truth for this transfer.
Announce and finish
The coordinator sends the decision to both participants. Each one commits or aborts, releases its locks and replies done. The coordinator keeps resending until every participant has replied.
Notice the one-way door in the middle. Before voting, a participant may abort freely. After voting yes, it has given up that right. It must do whatever the coordinator decides, even after a crash and restart.
The coordinator in Python
Here is the coordinator as a small program you can run. Each Participant object stands in for one database. A plain list stands in for the coordinator's log on disk.
class Participant:
def __init__(self, name, can_commit=True, replies=True):
self.name, self.can_commit, self.replies = name, can_commit, replies
self.state = "WORKING"
def prepare(self, txn_id, timeout):
if not self.replies:
raise TimeoutError # stand-in for a vote that never arrives
self.state = "PREPARED" if self.can_commit else "ABORT"
return "YES" if self.can_commit else "NO"
def finish(self, txn_id, decision):
self.state = decision
def run_transfer(txn_id, participants, log):
votes = []
for p in participants: # phase 1: prepare and vote
try:
votes.append(p.prepare(txn_id, timeout=2))
except TimeoutError:
votes.append("NO") # no answer counts as no
decision = "COMMIT" if all(v == "YES" for v in votes) else "ABORT"
log.append((txn_id, decision)) # real code: a forced write to disk
for p in participants: # phase 2: announce
p.finish(txn_id, decision) # real code: retry until p replies
return decision
log = []
print(run_transfer("t1", [Participant("savings"), Participant("wallet")], log))
print(run_transfer("t2", [Participant("savings"), Participant("wallet", can_commit=False)], log))
print(run_transfer("t3", [Participant("savings"), Participant("wallet", replies=False)], log))
print(log)What just happened? On Python 3.13 it prints COMMIT, ABORT, ABORT, then the log with all three decisions. Transfer t2 aborted because the wallet voted no. Transfer t3 aborted because the wallet never answered.
A missing vote counts as no, which is always safe because nobody has committed yet. The log line runs before any commit message leaves, so a restarted coordinator can read the decision back.
Why does two-phase commit block?
Two-phase commit blocks because a participant that has voted yes cannot decide on its own. If the coordinator crashes before announcing the decision, the participant must wait, holding its locks, until it learns the outcome.
The time between voting yes and hearing the decision is the uncertainty window. A participant inside it is in doubt. It does not know how the other participant voted, whether the coordinator chose commit or abort, or whom it already told.
Look at the wallet database inside that window. If it aborts, maybe the coordinator already told the savings side to commit. Then the debit is permanent and the customer loses Rs 5,000.
If it commits instead, maybe the savings side voted no and the decision was abort. Then the wallet holds Rs 5,000 that was never debited. Either guess can break the rule, so the only safe move is to wait.
Even both voting yes does not settle it. The coordinator may have timed out on a late vote and chosen abort, then crashed before telling anyone.
We checked this with a small model that tries every vote, decision and coordinator crash point. With both sides prepared and uninformed, the logged decision can be commit or abort.
This is a property of the protocol, not a bug in one product. Dale Skeen's 1981 paper on nonblocking commit calls two-phase commit a blocking protocol. Jim Gray and Leslie Lamport's 2004 paper says the same about coordinator failure.
Here is an everyday picture. A matchmaker is fixing a roka, the ceremony where two families formally agree to a match. He visits each family for a firm yes, and only then announces the roka.
The matchmaker is the coordinator, each family is a participant, and the firm yes is the vote. Turning away other proposals while waiting is holding the locks. If he goes missing after both yeses, neither family can announce or move on.
Where the picture breaks: two families who both said yes could phone each other and go ahead. Two databases cannot, because the coordinator may already have chosen abort, for example after a late vote.
The bank met this four months after switching to 2PC. The coordinator's server lost power in the evening rush, and 210 transfers were in doubt for the 40 minutes it took to come back (numbers made up).
For those 40 minutes, the affected savings and wallet rows stayed locked. New payments from those accounts queued behind the locks, then failed. The support team took hundreds of calls that evening.
Where two-phase commit gets stuck
Drag to exploreBoth databases voted yes, then the coordinator vanished. Notice that the question box has no arrow back to either database: neither can answer it alone.
Diagram connections:
- Transfer coordinator to Savings database (Prepared, voted yes)
- Transfer coordinator to Wallet database (Prepared, voted yes)
- Savings database to Commit or abort?
- Wallet database to Commit or abort?
- Commit or abort? to Wait and keep the locks (No one can answer)
- Wait and keep the locks to Operations team (Alert after 5 minutes)
- Transfer coordinator: Crashed after collecting both yes votes
- Savings database: Prepared: Rs 5,000 debit saved, row locked
- Wallet database: Prepared: Rs 5,000 credit saved, row locked
- Commit or abort?: Only the coordinator's log knows
- Wait and keep the locks: New payments on these rows queue up
- Operations team: Sees frozen accounts and angry calls
How do you recover from a stuck two-phase commit?
You recover by getting the real decision to every participant. Restart the coordinator so it reads its log, or ask another participant that already knows. If nobody knows, a person checks the evidence and decides.
- Restart the coordinator fast. On restart it reads its log and resends every saved decision. A transfer with no saved decision is aborted, because a commit message is only sent after the decision is saved.
- Ask a peer. A participant in doubt can ask the others. One that already heard the decision can share it.
- Watch the in-doubt list. Databases list their prepared transactions, and the engineer section shows the exact query. Alert when any entry is older than a few minutes.
- Fix by hand only with evidence. An operator may force a commit or a rollback, but only after checking the coordinator's log and the other database.
Timeouts help only before the vote. A participant that has not voted yet can abort on a timeout, because the coordinator needs its yes to commit. After voting yes, a timeout can only start asking questions.
Here is that rule as runnable code. Each participant's agent calls it when it has waited too long. The dictionaries stand in for the other machines and what each one knows.
def on_timeout(my_state, others):
if my_state == "WORKING": # I have not voted yet
return "ABORT" # safe: no commit can happen without my yes
for node in others: # I voted yes, so ask around
if not node["up"]:
continue
if node["state"] in ("COMMIT", "ABORT"):
return node["state"] # it heard the decision
if node["state"] == "WORKING":
return "ABORT" # it never voted, so it aborts, and so can I
return "WAIT" # everyone I can reach is in doubt too
coordinator = {"up": False, "state": "COMMIT"} # crashed; its log is out of reach
print(on_timeout("WORKING", [coordinator]))
print(on_timeout("PREPARED", [coordinator, {"up": True, "state": "COMMIT"}]))
print(on_timeout("PREPARED", [coordinator, {"up": True, "state": "WORKING"}]))
print(on_timeout("PREPARED", [coordinator, {"up": True, "state": "PREPARED"}]))What just happened? It prints ABORT, COMMIT, ABORT, WAIT. The last case is the blocking problem in one line: everyone reachable is in doubt too, so the only safe answer is to wait.
The loop is called cooperative termination. A peer that has not voted yet will abort on the spot and answer ABORT, since the coordinator can no longer collect its yes.
What is three-phase commit, and what does it try to fix?
Three-phase commit, or 3PC, adds a middle round so that no participant stays stuck in doubt after a coordinator crash. Survivors can then finish the transaction on their own, if the network behaves.
Dale Skeen described it in his 1981 paper on nonblocking commit. The key idea is simple. Before anyone commits, everyone must first hear that everyone voted yes.
- Vote. The coordinator asks each participant whether it can commit. Each votes yes or no, like prepare in 2PC.
- Pre-commit. If all voted yes, the coordinator tells each one that everyone agreed. Each participant acknowledges the message.
- Commit. After all acknowledgements arrive, the coordinator tells everyone to commit.
The middle round changes what a lone participant knows. The coordinator sends commit only after every participant has the pre-commit. So if a survivor never got pre-commit, nobody has committed yet.
That gives survivors a rule when the coordinator goes silent. They pick a new leader and compare notes. If any of them committed or got pre-commit, they all commit. Otherwise they all abort.
We checked this rule in the same small model, with three participants. Across every crash point and every set of crashed participants (273 cases), one round of the rule never contradicted a participant that had already committed or aborted.
Three-phase commit and its timeout rule
Drag to exploreThe pre-commit round tells everyone that everyone said yes. Notice the question box: survivors use it only when the coordinator goes silent.
Diagram connections:
- Phase 1: Vote to commit to Phase 2: Pre-commit (All voted yes)
- Phase 2: Pre-commit to All acknowledgements in
- All acknowledgements in to Phase 3: Do commit
- Phase 2: Pre-commit to Survivor past pre-commit? (Coordinator goes silent)
- Survivor past pre-commit? to Commit without coordinator (Yes)
- Survivor past pre-commit? to Abort without coordinator (No)
- Phase 1: Vote to commit: Each database votes yes or no
- Phase 2: Pre-commit: Everyone voted yes, get ready
- All acknowledgements in: Coordinator knows every node is ready
- Phase 3: Do commit: Each database commits and replies
- Survivor past pre-commit?: Survivors compare notes on timeout
- Commit without coordinator: Everyone voted yes, so commit is safe
- Abort without coordinator: No one can have committed yet
Why does three-phase commit rarely fix blocking in practice?
Three-phase commit is safe only when a silent machine has really crashed. Real networks also delay and drop messages, and during a network split 3PC can commit on one side and abort on the other.
Skeen's paper assumes the network never fails and that a site's failure can be detected. In practice, a network split or a long pause looks exactly like a crash from outside.
A network split, also called a partition, is when machines keep running but some of them cannot reach others. Each group thinks the other group died.
Picture a split in the bank setup. Both databases vote yes. The coordinator sends pre-commit to the wallet, then the link to the savings side breaks, and the coordinator crashes.
Pause and predict: each side applies the survivor rule using only the machines it can reach. What does each side decide?
The wallet saw a pre-commit, so it commits. The savings side saw none, so it aborts. Our model found the same disagreement, with the two sides swapped.
How a network split breaks 3PC
Drag to exploreEach side follows the same survivor rule correctly, yet the two sides reach opposite decisions. Notice that nothing here crashed except the coordinator.
Diagram connections:
- Transfer coordinator to Wallet database (Pre-commit delivered)
- Transfer coordinator to Network split (Pre-commit lost)
- Network split to Savings database
- Savings database to Rs 5,000 out of thin air (Aborts on timeout)
- Wallet database to Rs 5,000 out of thin air (Commits on timeout)
- Transfer coordinator: Sent pre-commit to the wallet, then crashed
- Network split: Savings side cut off from the rest
- Savings database: Voted yes, no pre-commit, so it aborts
- Wallet database: Got pre-commit, so it commits
- Rs 5,000 out of thin air: Debit undone, wallet credited anyway
Plain 2PC would have waited instead. Waiting is painful, but the protocol itself never moves money wrongly. 3PC swaps blocking for a risk of split decisions, which a bank cannot accept.
3PC also costs more when nothing fails. It needs three rounds of messages instead of two, so every commit waits for an extra round trip (a message out and its reply back).
Later protocols add quorums, meaning a majority of machines, to handle splits. Keidar and Dolev's enhanced three-phase commit (E3PC, 1998) refines Skeen's later quorum-based 3PC so that a connected quorum can always make progress. These ideas sit close to consensus, covered below.
How do PostgreSQL and MySQL hold a prepared transaction?
Both databases expose the participant side of 2PC directly. You prepare a transaction under a name you choose, and a later session can commit or roll it back by that name.
PostgreSQL prepared transactions
PREPARE TRANSACTION takes the open transaction away from your session and stores its state on disk. Later, another session can finish it with COMMIT PREPARED or ROLLBACK PREPARED, if it runs as the same user or a superuser, an admin account that skips permission checks.
The feature is off by default, because max_prepared_transactions starts at 0 and can change only at server start. The documentation says the command is meant for an external transaction manager, not for application code.
We ran the whole cycle on PostgreSQL 16.15 to check the claims above. These are the commands and the output we got, trimmed to the lines that matter.
# PostgreSQL 16.15, as of 22 October 2026. Out of the box:
psql -c "SHOW max_prepared_transactions;" # 0
psql -c "BEGIN; UPDATE accounts SET balance_rs = balance_rs - 5000 WHERE id = 1; PREPARE TRANSACTION 'transfer-42';"
# ERROR: prepared transactions are disabled
# HINT: Set max_prepared_transactions to a nonzero value.
# Restart with max_prepared_transactions = 10, run the same line, then from a NEW session:
psql -c "SELECT id, balance_rs FROM accounts WHERE id = 1;" # 20000 (old value)
psql -c "SELECT gid, owner FROM pg_prepared_xacts;" # transfer-42 | postgres
psql -c "SET lock_timeout = '2s'; UPDATE accounts SET balance_rs = balance_rs + 1 WHERE id = 1;"
# ERROR: canceling statement due to lock timeout
# Crash the server (pg_ctl stop -m immediate), start it, and check again:
psql -c "SELECT gid FROM pg_prepared_xacts;" # transfer-42 is still there
psql -c "COMMIT PREPARED 'transfer-42';"
psql -c "SELECT id, balance_rs FROM accounts WHERE id = 1;" # 15000What just happened? The prepared change stayed invisible, and its row lock outlived the session that took it. In pg_locks, PostgreSQL's list of held locks, its locks had an empty pid column, meaning no session owned them. It also survived an immediate shutdown, the kind a crash causes.
Watch for stuck ones with this query. A transaction ID is the number PostgreSQL gives each transaction that writes. The age function counts how many were handed out since the prepared transaction got its ID.
psql -c "SELECT gid, now() - prepared AS waiting_for, age(transaction) AS xids_since
FROM pg_prepared_xacts
WHERE prepared < now() - interval '5 minutes';"
# Healthy: (0 rows). Any row is a transaction in doubt for over 5 minutes.Old prepared transactions also hurt clean-up. VACUUM, PostgreSQL's clean-up process, cannot remove rows deleted after a still-open prepared transaction began.
We measured it on 16.15. With one prepared transaction open, VACUUM on a table with 1,000 deleted rows reported 1000 are dead but not yet removable. After ROLLBACK PREPARED, the same command removed all 1,000.
Left for a very long time, it can push the database towards transaction ID wraparound, when its 32-bit transaction counter runs out of safe values. The PREPARE TRANSACTION page warns the database could then shut down.
The vacuuming chapter is more exact. Close to the limit, PostgreSQL refuses to hand out new transaction IDs, so only read-only transactions can start. Its recovery steps include resolving old prepared transactions.
MySQL XA transactions
MySQL offers the same idea through XA transactions, based on the X/Open XA specification, a published standard for distributed transactions. Its 8.4 manual calls the coordinator a transaction manager and each database's share of the work a branch.
In the first phase every branch prepares, typically by recording its actions in stable storage (storage that survives a crash). XA RECOVER lists every transaction in the PREPARED state, whichever client started it, for a user with the XA_RECOVER_ADMIN privilege.
We ran XA START, XA END and XA PREPARE on MySQL 8.0.46, the last 8.0 release, then disconnected. A new session saw the transaction in XA RECOVER, read the old balance and timed out on the row lock.
A third session then ran XA COMMIT and the new balance appeared. MySQL 8.0 reached end of life in April 2026, and we did not run this on 8.4. The 8.4 manual says a prepared XA transaction persists until an explicit XA COMMIT or XA ROLLBACK. Recheck this on the version you run.
How do real systems stop the coordinator from blocking everyone?
Distributed databases built for this replicate the coordinator's decision with consensus, so no single machine holds it. Consensus means a group of machines agreeing on one value even if a few of them fail.
Paxos and Raft are two well-known consensus methods. They keep working while a majority of their machines are up and can reach each other.
Gray and Lamport's Paxos Commit runs a consensus step on each participant's vote. It uses 2F+1 coordinators and keeps working while any F+1 of them are up. F is the number of coordinator failures you want to survive.
The paper says Paxos Commit has the same disk-write delay as 2PC. Its basic version takes one extra message delay, five against four. The short abstract on arXiv mentions only this version.
The paper then shows how to remove that delay, so the normal case matches 2PC. The price is more messages: about 2FN extra, for N participating databases.
Classic 2PC is the special case where F is zero. That one line explains the blocking problem: zero spare coordinators means zero tolerance for a coordinator failure.
Google Spanner, described in its 2012 paper, puts this into production. Each group of data replicas runs Paxos. A transaction across groups runs 2PC between the group leaders, and one group acts as coordinator.
The coordinator's 2PC state is stored in its Paxos group, so it is replicated. In the paper's words, running two-phase commit over Paxos mitigates the availability problems.
The bank in our setup did a smaller version of this. It moved the decision log to replicated storage and added a standby coordinator that reads the same log.
One detail decides whether that is safe. The log must accept only one decision per transaction, for example with a conditional write. Otherwise an old coordinator that wakes up could record a different decision.
When should you use two-phase commit, and what are the alternatives?
Use two-phase commit for short transactions across a few databases you run, where an in-between state must never be visible. For long flows across services or partners, use a saga instead.
A saga takes the opposite road. Each step commits on its own database right away. If a later step fails, a compensating step, which is an undo action, reverses the earlier ones. Nothing waits on cross-database locks, but customers can briefly see in-between states.
| Approach | Message rounds | If the coordinator dies | If the network splits | Good fit |
|---|---|---|---|---|
| Two-phase commit | 2 rounds | Prepared databases wait, locks held | Stays correct, may wait | A few databases you run, short transactions |
| Three-phase commit | 3 rounds | Survivors finish alone | Sides can decide differently | Only where delays have a known bound |
| 2PC over consensus (Paxos Commit, Spanner) | Same as 2PC when optimised, with more messages | Another coordinator replica finishes | A majority of coordinator replicas can still decide | Distributed databases built on consensus |
| Saga | One local commit per step | No cross-database locks to wait on | Steps retry or undo later | Long flows across services and partners |
Signs that 2PC fits your problem:
- Two or three databases, all run by your team, ideally in the same data centre.
- Each transaction is short, a few rows and a few milliseconds of work.
- Every participant can prepare, such as PostgreSQL prepared transactions or MySQL XA.
- A visible half-finished state would be worse than a short wait.
Signs that you should avoid it:
- A step depends on an outside partner, such as another bank's payment network.
- A step takes minutes or hours, like waiting for a person to approve.
- A step cannot be prepared and held, like sending an SMS, which nobody can unsend.
- Very busy rows, where locks held during a stall would hurt thousands of users.
The bank split its problem this way. Savings to wallet stays on 2PC, because both databases sit in one data centre and the team runs both.
Transfers to other banks use a saga instead. The service debits the savings account first, then sends the payment out. If the other bank rejects it, a compensating step refunds the customer.
How do you explain two-phase commit in a system design interview?
Explain it as a vote followed by an announcement, then spend most of your time on what happens when the coordinator fails.
State the goal
One change across two databases must be atomic: both commit or both abort. Use a concrete case, like a debit in one ledger and a credit in another.
Draw the two phases
Prepare and vote, then decide and announce. Point at the coordinator's log write as the exact commit point.
Show the in-doubt window
Explain why a participant that voted yes cannot decide alone, and why it must hold its locks while it waits.
Describe recovery
The coordinator rereads its log on restart, peers share what they know, and timeouts abort only before the vote. Mention alerting on old prepared transactions.
Bring in 3PC carefully
Say what the pre-commit round adds, then say why a network split breaks it.
Offer the real fixes
A replicated coordinator using consensus, as in Paxos Commit or Spanner, or a saga with compensating steps when brief in-between states are acceptable.
Questions people ask
- Is two-phase commit the same as two-phase locking?
- No. Two-phase locking is a rule inside one database: a transaction takes all its locks before it releases any, so concurrent transactions do not clash. Two-phase commit is a protocol between databases that makes them commit or abort together. They often appear together, because a prepared participant keeps its locks until the decision arrives.
- Why is two-phase commit called a blocking protocol?
- Because a participant that has voted yes may have to wait with no time limit. If the coordinator crashes before sending the decision, the participant cannot safely commit or abort alone. It keeps its locks until the coordinator recovers or another participant tells it the outcome.
- Can a timeout solve the 2PC blocking problem?
- Only partly. Before a participant votes, a timeout can safely trigger an abort. After it votes yes, a timeout cannot choose the outcome, because the coordinator may already have told others to commit. It can only start asking the coordinator and the other participants what happened.
- What is an in-doubt transaction?
- An in-doubt transaction is one that a participant has prepared but whose final outcome it does not know yet. In PostgreSQL, such transactions appear in the pg_prepared_xacts view until someone runs COMMIT PREPARED or ROLLBACK PREPARED. In MySQL, XA RECOVER lists them. Watch these lists, because old entries hold locks and hold back clean-up.
- Is two-phase commit a consensus algorithm?
- It is a narrow kind of agreement, but not fault-tolerant consensus. Two-phase commit needs a yes from every participant and depends on one coordinator. Consensus methods such as Paxos and Raft need only a working majority. Gray and Lamport showed that classic 2PC is the special case of their Paxos Commit that tolerates zero coordinator failures.
- Should microservices use two-phase commit?
- Usually not. Microservices often call databases or outside APIs that cannot prepare, and holding locks across network calls hurts every service involved. A common alternative is a saga: each service commits its own step, and compensating steps undo earlier ones if a later step fails. Keep 2PC for a few databases you control.
Sources and further reading
- PostgreSQL documentation (current, version 18 as of October 2026): PREPARE TRANSACTION
- PostgreSQL documentation (current, version 18 as of October 2026): COMMIT PREPARED, who may commit
- PostgreSQL documentation (current, version 18 as of October 2026): Resource Consumption, max_prepared_transactions
- PostgreSQL documentation (current, version 18 as of October 2026): the pg_prepared_xacts view
- PostgreSQL documentation (current, version 18 as of October 2026): Routine Vacuuming, transaction ID wraparound
- MySQL 8.4 Reference Manual: XA Transactions
- MySQL 8.4 Reference Manual: XA Transaction SQL Statements (XA RECOVER)
- MySQL 8.4 Reference Manual: Restrictions on XA Transactions (prepared state persists)
- MySQL 8.0 Release Notes: 8.0.46 and the end of life of 8.0 (April 2026)
- Nonblocking Commit Protocols (Skeen, SIGMOD 1981), course copy at Purdue
- Consensus on Transaction Commit (Gray and Lamport, 2004), arXiv
- Spanner: Google's Globally-Distributed Database (OSDI 2012), sections 2.1 and 2.3
- Concurrency Control and Recovery in Database Systems (Bernstein, Hadzilacos and Goodman, 1987), chapter 7, section 7.4: cooperative termination
- Atomic Commit lecture slides, CSE 223A, UC San Diego (2009): cooperative termination and the 3PC termination rule
- Increasing the Resilience of Distributed and Replicated Database Systems (Keidar and Dolev, JCSS 1998), author preprint: E3PC
- Three-phase commit protocol (Wikipedia, secondary): the bounded-delay assumption
- Sagas (Garcia-Molina and Salem, Princeton tech report, 1987)