Two-Phase Commit (2PC) is a distributed protocol that guarantees atomicity across multiple database nodes. When a transaction touches data on more than one shard—for example, updating a user’s profile on shard A and their billing record on shard B—2PC ensures that either both updates commit or both roll back. There is no state where one shard has committed and another has not.
In AI infrastructure, 2PC matters whenever a single logical operation spans multiple storage systems. Consider a RAG pipeline that must atomically update a document in the vector store and its metadata in a relational database, or a multi-tenant system where embedding insertion and quota accounting must succeed or fail together. Without an atomic commit protocol, partial failures leave the system in inconsistent states that are difficult to detect and expensive to repair.
2PC was formalized by Jim Gray in 1978 and remains the foundation of distributed transaction protocols in systems like PostgreSQL (for distributed transactions via prepared transactions), Google Spanner, CockroachDB, and TiDB. Understanding its mechanics—and its limitations—is essential for designing reliable distributed AI infrastructure.
2PC involves two roles:
2PC ensures that all participants either commit or abort. The coordinator collects votes from every participant before issuing a global decision. A single “no” vote from any participant causes the entire transaction to abort across all nodes.
The coordinator sends a PREPARE message to every participant involved in the transaction. Each participant then:
VOTE_COMMIT (yes, I can commit) or VOTE_ABORT (no, something went wrong).// Phase 1: Coordinator sends PREPARE to all participants
for _, participant := range participants {
response := participant.Prepare(txn)
votes = append(votes, response)
}
// Coordinator collects votes
allYes := true
for _, vote := range votes {
if vote != VOTE_COMMIT {
allYes = false
break
}
}
At this point, each participant has made a durable promise: if it voted yes, it guarantees it can commit the transaction even if it crashes and restarts. The WAL entry ensures recovery is possible.
Based on the votes, the coordinator makes an irrevocable decision:
COMMIT record to its own WAL, then sends COMMIT to every participant.ABORT to every participant.// Phase 2: Coordinator sends decision
if allYes {
coordinator.WriteToWAL("COMMIT", txn.ID)
for _, participant := range participants {
participant.Commit(txn)
}
} else {
for _, participant := range participants {
participant.Abort(txn)
}
}
Each participant, upon receiving the decision, applies it (committing or rolling back) and releases its locks. It then sends an ACK back to the coordinator. Once all acknowledgments are received, the coordinator can garbage-collect the transaction record.
The most significant limitation of 2PC is that it is a blocking protocol. If the coordinator crashes after participants have voted COMMIT but before sending the commit decision, participants are stuck. They cannot commit (they don’t know if all other participants also voted yes) and they cannot abort (they already promised to commit). They must hold their locks and wait indefinitely for the coordinator to recover.
Between Phase 1 and Phase 2, the coordinator holds the sole copy of the transaction’s fate. If it fails at this moment, all participants block. This is why production systems pair 2PC with coordinator high availability (e.g., a Raft-replicated transaction log) to minimize this window of vulnerability.
Participant crashes before voting: The coordinator times out waiting for the vote and aborts the transaction. Safe and straightforward.
Participant crashes after voting yes: On recovery, the participant reads its WAL, sees it voted COMMIT, and contacts the coordinator (or other participants) to learn the outcome. It then commits or aborts accordingly.
Coordinator crashes after collecting votes: This is the dangerous case. If a new coordinator is elected, it must contact all participants to determine who voted what. If even one participant is also down, the new coordinator cannot determine the outcome and must wait.
Network partition: If the coordinator is on one side of a partition and some participants on the other, the isolated participants block until the partition heals.
Three-Phase Commit (3PC) adds a PRE_COMMIT phase between Prepare and Commit. After all participants vote yes, the coordinator sends PRE_COMMIT to confirm that everyone is prepared. Only then does it send the final COMMIT. This eliminates the ambiguous state where a coordinator crash leaves participants uncertain. 3PC is non-blocking—it places an upper bound on how long a transaction can be in doubt.
However, 3PC adds message complexity (3 round trips instead of 2) and is vulnerable to network partitions in ways that can violate safety. In practice, most distributed databases use 2PC combined with Paxos or Raft-based coordinator replication rather than 3PC.
Modern systems have evolved 2PC to mitigate its weaknesses:
When a document is re-embedded (e.g., after content changes), the system must atomically update the vector in the embedding store and the metadata (version, timestamp, chunk boundaries) in the document database. 2PC ensures that stale embeddings are never served with updated metadata or vice versa.
If a document’s chunks are distributed across multiple vector database shards (because different chunks hash to different shards), inserting or deleting the complete document requires a cross-shard transaction. 2PC ensures either all chunks are inserted/deleted or none are, preventing orphaned vectors that return irrelevant search results.
A multi-tenant AI platform that meters API usage (embedding calls, search queries) must atomically execute the operation and decrement the tenant’s quota. Without 2PC, a crash after executing the operation but before recording the usage means the tenant gets free API calls; a crash after recording but before executing means they are charged for nothing.