Paxos Consensus Protocol
Imagine a bank replicates its account database across five data centers. This ensures no single failure erases customer balances. Every replica must apply the same sequence of updates in the same order. This must hold even when machines crash mid-transaction. It must also hold when the network drops or delays messages arbitrarily. This is the Consensus Problem in its most demanding form. Agreement must hold not in the friendly case, but in the adversarial case. In that case, any minority of participants may fail silently at the worst possible moment. The FLP Impossibility Result tells us something important. No deterministic algorithm can guarantee both safety and termination in a fully asynchronous system. This holds true even with just one faulty process. Paxos accepts this limit rather than fighting it. It guarantees safety always — the system never disagrees or corrupts its history. This holds no matter how badly the network behaves. It guarantees liveness only under sufficiently good conditions, such as a stable elected leader and bounded message delays. This asymmetric guarantee is precisely what real systems need. Correctness is non-negotiable, but progress can wait a few extra rounds during a storm. Paxos was introduced by Leslie Lamport. It is the ancestor of nearly every production consensus protocol in use today. Google's Chubby lock service, Apache ZooKeeper's ZAB protocol, Google Spanner's replicated log, and the log-replication core of Raft all descend from Paxos. Others were designed in direct response to it. Understanding Paxos means understanding why replicated logs, leader election, and quorum systems work the way they do. It is not just about knowing that they work. This article builds Paxos from first principles. It covers the roles of proposer, acceptor, and learner. It explains the two-phase protocol that lets a value survive crashes and restarts. It also gives a rigorous argument for why any two acceptors that decide, decide the same value. It closes by tracing the path toward Raft Consensus Algorithm and modern Replication and Consistency Models. These systems inherit Paxos's safety core. At the same time, they work hard to make its operation more comprehensible.
- Paxos solves consensus among unreliable processes: all non-faulty nodes agree on one value, safely, despite crashes and message loss.
- Three roles — proposer, acceptor, learner — can be co-located on the same physical nodes; separating them clarifies the protocol's logic.
- Consensus proceeds in two phases per round: Prepare/Promise (reserve a numbered round) then Accept/Accepted (propose a value in that round).
- Safety rests on a single invariant: once a majority of acceptors accept a value in some round, every higher round is forced to propose that same value.
- Paxos guarantees safety unconditionally; it only guarantees termination (liveness) when the network and a leader behave well enough, consistent with FLP Impossibility Result.
- Multi-Paxos chains many single-value Paxos instances into a replicated log, forming the backbone that Raft Consensus Algorithm later re-engineered for teachability.
Paxos achieves safe agreement by forcing every proposal round to first discover, and then re-propose, any value a majority may have already chosen.
The idea
Paxos separates the consensus problem into three logical roles. A proposer suggests a value it wants the system to agree on. An acceptor is a voting member that can promise not to accept old proposals and can accept new ones; agreement is defined in terms of what a majority (quorum) of acceptors accept. A learner simply finds out what value was chosen, typically by listening to acceptors or being told by the proposer that succeeded. In most deployments a single physical server plays all three roles simultaneously, but keeping the roles conceptually distinct is what makes the correctness argument tractable. Every proposal carries a strictly increasing round number (also called a ballot number or proposal number), unique to the proposer that issues it — typically constructed as `(counter, proposer_id)` so that ties never occur across different proposers. The protocol's central mechanism is that a higher round number can always interrupt a lower one, but any proposer using a higher round is obligated to re-propose whatever value a majority may already have locked in, rather than inventing a new value freely. This is the crux of the design: rounds provide liveness through retry (a stalled proposer can always try again with a bigger number), while the value-carrying obligation provides safety (no round can silently overwrite a decision already made). A value is considered chosen once a majority of acceptors have accepted it in the same round. Because any two majorities among acceptors must overlap in at least one acceptor (this is the Quorum Intersection property), no two different values can ever both be chosen — the overlapping acceptor cannot have accepted two different values in the round that chose them, and the protocol's rules ensure it cannot be tricked into doing so across rounds either. This single overlap fact, formalized below, is the entire safety proof of Paxos.
How it works
Single-decree Paxos decides one value. It runs in two phases, each itself a request/response round between a proposer and the acceptors.
Phase 1 (Prepare / Promise). The proposer picks a round number higher than any it has used before, and sends `Prepare(n)` to a majority of acceptors. An acceptor that receives `Prepare(n)`:
- If is greater than every round number it has seen so far, it replies `Promise(n, acceptedRound, acceptedValue)` — promising never again to accept any proposal numbered less than — and reports the highest-numbered value it has already accepted, if any.
- Otherwise it ignores the request (or replies with a rejection carrying the higher round it already knows about, so the proposer can jump ahead).
Phase 2 (Accept / Accepted). If the proposer receives `Promise` replies from a majority, it must choose the value to propose:
- If any acceptor reported a previously accepted value, the proposer must adopt the value from the highest `acceptedRound` among the replies — it is not free to substitute its own value.
- Only if no acceptor reported any previously accepted value may the proposer use its own value.
It then sends `Accept(n, value)` to the same majority. An acceptor that receives `Accept(n, value)` accepts it (recording `acceptedRound = n`, `acceptedValue = value`) unless it has already promised a higher round number in Phase 1, in which case it rejects the request. Once a majority accepts , that value is chosen and can be revealed to learners — typically by having acceptors notify learners directly, or by having the proposer collect enough `Accepted` messages and broadcast the decision itself. ```pseudocode
# --- Proposer --- function Propose(proposer_id, value_hint): n = next_round_number(proposer_id) # e.g., (counter++, proposer_id), globally unique & increasing promises = []
# Phase 1: Prepare send Prepare(n) to all acceptors wait for replies from a majority of acceptors, or timeout for each reply Promise(n, acc_round, acc_value) received: promises.append((acc_round, acc_value)) if reply is Reject(higher_n): n = higher_n + bump() # back off and retry with a bigger number goto Phase 1 if len(promises) < majority_count: retry with a larger n after backoff # no quorum reached -> contention or failures
# Determine the value to propose if any promise carries a non-null acc_value: value = value_with_highest(acc_round, promises) # MUST reuse this value else: value = value_hint # free to propose own value
# Phase 2: Accept send Accept(n, value) to the same majority wait for Accepted(n) replies, or timeout / Reject if majority of Accepted(n) received: value is CHOSEN notify learners(value) return value else: retry with a larger n after backoff
# --- Acceptor state (persisted to stable storage before replying) --- promised_round = -infinity accepted_round = -infinity accepted_value = null function OnPrepare(n): if n > promised_round: promised_round = n persist(promised_round) reply Promise(n, accepted_round, accepted_value) else: reply Reject(promised_round) function OnAccept(n, value): if n >= promised_round: promised_round = n accepted_round = n accepted_value = value persist(promised_round, accepted_round, accepted_value) reply Accepted(n) else: reply Reject(promised_round) ``` Two subtleties matter for correctness. First, an acceptor must persist its state to stable storage before replying — otherwise a crash-and-restart could forget a promise and violate safety. Second, the `OnAccept` check uses `n >= promised_round` rather than strict equality: an acceptor may `Accept` a proposal numbered exactly equal to its highest promise, since that promise was made for this very proposer's Phase 2 message.
To decide many values in sequence — the realistic case of a replicated log — systems run Multi-Paxos: a single elected proposer (the leader) runs Phase 1 once to reserve a round number for an entire range of log slots, then repeatedly runs only Phase 2 for each new log entry, skipping Phase 1 as long as it remains the recognized leader. This amortizes the expensive round-trip of Phase 1 across many decisions, reducing steady-state latency to one round trip per committed value.
Complexity & correctness
Message and round complexity. In the common case with no contention, single-decree Paxos takes exactly two round trips: Prepare/Promise, then Accept/Accepted, each fanning out to a majority of acceptors. This is messages per phase, so total messages and round trips when uncontested. Under contention — two proposers dueling with increasing round numbers — the protocol can, in principle, retry indefinitely (the classical "dueling proposers" livelock), which is exactly the manifestation of FLP Impossibility Result: no bound on termination can be guaranteed in a fully asynchronous, failure-prone system. Practical deployments break this livelock by electing a stable leader (via randomized backoff or a separate leader-election protocol), reducing steady state to round trips per decision.
Fault tolerance. With acceptors, Paxos tolerates up to crash failures while still being able to form a majority quorum of . It does not tolerate Byzantine (malicious/arbitrary) failures without extension — the base protocol assumes acceptors follow the algorithm faithfully but may crash and restart.
Space. Each acceptor stores only state — its highest promised round and its single highest accepted `(round, value)` pair — regardless of how many proposers have competed, because the protocol only ever needs the latest promise and latest acceptance to answer future messages correctly.
Correctness proof sketch (safety). The goal is: if a value is chosen (accepted by a majority) in round , then any value chosen in any round is also . The proof is by strong induction on round number. Invariant P1 (quorum intersection). Any two majority sets of acceptors, out of a fixed total set, share at least one common acceptor. This is pure counting: if out of acceptors, then . Invariant P2. If a proposer issues `Accept(n, v)`, then either no acceptor has accepted any value in a round less than , or is the value with the highest-numbered prior acceptance reported to that proposer in its Phase 1 promises. Invariant P2a. If `Accept(n, v)` can be issued, then equals the value of every value chosen (by a majority) in any round less than .
Base case: Let be the smallest round in which a value is chosen (accepted by majority ). By definition no earlier chosen value exists, so vacuously P2a holds for .
Inductive step: Suppose P2a holds for all rounds less than , and suppose `Accept(n, v')` is issued by some proposer . We must show for the (unique, by induction hypothesis extended) value chosen in . By the Phase 1 rule, collected `Promise` replies from a majority before issuing `Accept(n, v')`. By quorum intersection (P1), : some acceptor is in both the majority that chose in round and the majority that promised in round . Because accepted at round , and replied to `Prepare(n)` with `Promise` (meaning is greater than every round has promised, hence greater than at the time it accepted), 's reply must report `acceptedRound \ge n_0` and some accepted value. By the Phase 2 rule, is forced to adopt the value carried by the highest `acceptedRound` among all its promises — and since a chosen value's acceptance is reported by at least one acceptor in every subsequent majority (by repeated quorum intersection across every round between and ), that highest-reported value must be . Hence . This gives the headline safety guarantee: at most one value is ever chosen, and it is chosen permanently — no failure pattern, message reordering, or duplicate delivery can unchoose it or cause a different value to be chosen afterward. Liveness (that some value eventually gets chosen) is explicitly not guaranteed in general, matching FLP; it requires eventual synchrony and typically a stable leader in practice.
Tradeoff to be honest about: Paxos as described is famously hard to map onto engineering artifacts — the roles blur, log compaction and membership changes are not specified by the base algorithm, and "which proposer is the leader right now" is left to auxiliary mechanisms. This gap is exactly what motivated Raft Consensus Algorithm, which keeps Paxos's safety core but bakes in leader election and log matching as first-class, more mechanically checkable rules.
Worked example
Consider acceptors (majority = 3). Two proposers, (client wants value `"X"`) and (client wants value `"Y"`), race after a partial network partition.
| Step | Actor | Action | Acceptor states after step |
|---|---|---|---|
| 1 | `Prepare(n=1)` to | all three promise: `(promised=1, accepted=∅)` | |
| 2 | reply `Promise(1, ∅, ∅)` to | no prior value reported | |
| 3 | majority reached, no prior value ⇒ free to use `"X"`. Sends `Accept(1, "X")` to | — | |
| 4 | accepts: `(promised=1, accepted=1, val="X")` | — | |
| 5 | network hiccup | `Accept(1,"X")` to is delayed/lost | still only `(promised=1)` |
| 6 | unaware of , sends `Prepare(n=2)` to | ||
| 7 | : promise, reply `Promise(2, ∅, ∅)` (has not yet seen `Accept(1,"X")`) | `(promised=2)` | |
| 8 | same, `Promise(2, ∅, ∅)` | `(promised=2)` | |
| 9 | fresh, `Promise(2, ∅, ∅)` | `(promised=2)` | |
| 10 | majority (3), none report prior value ⇒ free to propose `"Y"`. Sends `Accept(2, "Y")` to | ||
| 11 | all promised ≤2, accept: `(promised=2, accepted=2, val="Y")` | majority now holds `"Y"` | |
| 12 | delayed `Accept(1,"X")` retried, but never promised past round 1 in this trace, so it would still accept a duplicate `Accept(1,"X")` — harmless, alone is not a majority | `(accepted=1,val="X")` unchanged in effect | |
| 13 | — | Result: `"Y"` is chosen (3 acceptors: ). `"X"` was never chosen (only 1 acceptor, not a majority) | consensus = `"Y"` |
Now suppose instead that step 5 had not lost the message — had already accepted `(1,"X")` before 's `Prepare(2)` arrived. Then in step 7, 's `Promise(2, ...)` reply would carry `acceptedRound=1, acceptedValue="X"`. At step 10, is no longer free to propose `"Y"`: the rule forces to adopt the highest-numbered reported value, which is `"X"` from . sends `Accept(2, "X")` instead, and the same value `"X"` ends up chosen — illustrating exactly the safety mechanism proved above: a higher round can steal the round number, but never the already-chosen value.
Common pitfalls & traps
| Pitfall | Why it breaks Paxos | Fix |
|---|---|---|
| Reusing round numbers across proposers | Two proposers with the same can race and violate the " strictly increasing, globally unique" assumption, breaking the safety proof's use of promises | Embed a unique proposer ID in the low bits of every round number |
| Acceptor replies before persisting to disk | A crash-restart can forget a promise, allowing a stale acceptor to later accept a conflicting value | Persist `promised_round`/`accepted_round`/`accepted_value` to stable storage before sending any reply |
| Proposer substitutes its own value despite a reported prior acceptance | Directly violates Invariant P2/P2a — this is the single most common implementation bug and can silently lose committed data | Always adopt the value from the highest `acceptedRound` among Promise replies; never override it |
| Treating "received Promise from a majority" as decision | A quorum of promises only reserves the round; the value is not chosen until a quorum accepts in Phase 2 | Only declare a value chosen after a majority of `Accepted` replies for the same round |
| Assuming Paxos guarantees termination | Contention between competing proposers can livelock forever (dueling proposers), consistent with FLP | Elect a stable leader with randomized backoff so only one proposer normally runs Phase 2 repeatedly |
| Skipping Phase 1 without ever having run it as the current leader | Multi-Paxos's optimization to skip Prepare is only safe once a leader owns an uncontested round number for that log range | Run Phase 1 for a full round on leader election before accepting client writes with the fast path |
Pioneers & history
Paxos originated from Leslie Lamport's whimsical framing of the problem as the parliamentary procedure of a fictional Greek island, "The Part-Time Parliament" — a paper so playfully written that its seriousness was initially missed by reviewers and readers alike. Lamport later republished a plainer version, "Paxos Made Simple," to correct the reception. The protocol is now considered one of the most important and most subtly difficult results in distributed computing, spawning an entire cottage industry of "Paxos made X" papers (live, simple, moderately complex, practical) attempting to make it more approachable to engineers.
Origins · 1 minLeslie B. Lamport and how this was discovered
Where this matters
Paxos and its Multi-Paxos extension underlie some of the most widely used distributed infrastructure in existence. Google's Chubby lock service and Spanner database use Paxos-replicated logs to keep metadata and transaction ordering consistent across data centers spanning continents. Apache ZooKeeper, used for coordination in Kafka, HBase, and countless distributed systems, runs ZAB, a protocol built on the same quorum-and-round principles as Paxos, tailored for a primary-backup log. Cloud object stores and distributed databases (CockroachDB, YugabyteDB, TiDB) use Paxos-family protocols, often via the Raft variant, to replicate ranges of key-value data with strong consistency guarantees. Understanding Paxos is prerequisite to understanding Raft Consensus Algorithm (a deliberately more teachable re-derivation of the same guarantees) and to reasoning correctly about Replication and Consistency Models used throughout modern cloud databases and coordination services.
Ask the tutor: How does Raft's leader-election term mechanism map onto Paxos's round numbers, and why does that mapping make Raft easier to reason about?
Is Paxos the same as the general consensus problem?
No. The consensus problem is the abstract specification — all correct processes must agree on one proposed value, and that value must have actually been proposed. Paxos is one concrete algorithm satisfying this specification under crash failures and asynchronous networks. Other algorithms, such as Raft or ZAB, satisfy the same specification with different internal mechanics, different engineering tradeoffs, and different degrees of implementation clarity. Understanding the consensus problem first (its safety and liveness requirements, and the impossibility results bounding it) is what lets you evaluate Paxos as one particular solution rather than as the definition of consensus itself.
Does Paxos tolerate Byzantine failures, like malicious nodes lying about their state?
No, not in its basic form. Classic Paxos assumes acceptors are "crash-stop": they may fail by halting, and may restart and rejoin, but they never deviate from the protocol while running — they don't lie about their promised round, forge messages, or intentionally violate the rules. Tolerating arbitrary, malicious deviation requires a different family of protocols, generally called Byzantine fault-tolerant (BFT) consensus, exemplified by PBFT and its descendants, which need replicas to tolerate Byzantine failures rather than the that plain Paxos needs for crash failures.
Why does an acceptor need to persist its state to stable storage before replying?
Because the safety proof relies on an acceptor never "forgetting" a promise or a prior acceptance. If an acceptor crashes after promising round but before writing that promise to disk, and then restarts and forgets it made that promise, it might later accept a lower-numbered proposal it should have rejected — reintroducing exactly the conflict the protocol is designed to prevent. Persisting `promised_round`, `accepted_round`, and `accepted_value` before sending any reply ensures a restarted acceptor resumes with full knowledge of every commitment it has made, preserving the quorum-intersection argument across crashes.
What is "dueling proposers" and how do real systems avoid it?
Dueling proposers is a livelock: proposer completes Phase 1 with round , but before it finishes Phase 2, proposer runs Phase 1 with round , invalidating 's in-flight Accept messages. retries with round , invalidating 's Phase 2, and so on forever — a direct real-world manifestation of the FLP Impossibility Result's limit on guaranteed termination. Real systems avoid this in practice, though never in theory, by electing a single stable leader (using randomized timeouts and backoff so simultaneous leader attempts become statistically rare) so that only one proposer typically runs Phase 2 repeatedly across many decisions.
How does Multi-Paxos differ from running single-decree Paxos repeatedly?
Running single-decree Paxos independently for each log slot would require two full round trips (Prepare/Promise then Accept/Accepted) per decision, doubling latency and message volume. Multi-Paxos instead has an elected leader run Phase 1 once, reserving its round number across an entire range of future log slots, and then runs only Phase 2 for each new client command, referencing the already-reserved round. This reduces steady-state commit latency to a single round trip per decision, at the cost of needing an explicit (if informal) leader-election and lease mechanism that base Paxos does not specify.
Why did Raft get invented if Paxos already solves consensus?
Paxos's safety guarantees are airtight, but the base algorithm intentionally under-specifies practical concerns: how leaders are elected, how logs are kept consistent and compacted, and how cluster membership changes safely. Engineers building real systems on Paxos had to invent these mechanisms themselves, often incompatibly, leading to notoriously subtle bugs. Raft Consensus Algorithm was designed explicitly for understandability: it decomposes the problem into leader election, log replication, and safety as separate, more mechanically verifiable pieces, while preserving the same fundamental safety guarantee proved for Paxos via quorum intersection.
Does Paxos require an odd number of acceptors?
It is not strictly required, but it is the efficient choice. With acceptors, majority size is , and fault tolerance is . Going from (odd) to (even) acceptors does not increase the number of tolerable failures at all — it only adds an acceptor without adding fault tolerance, while increasing quorum size and message overhead. Production systems therefore almost always deploy an odd number of acceptors, such as 3 or 5, to get the maximum fault tolerance per unit of replication cost.