Basic Paxos solves a single consensus instance: selecting at most one value from proposed values. Once a value is chosen, subsequent valid proposals cannot cause that instance to select a different value. To replicate a continuous command log, related instances can be run for different log slots, and protocol costs amortized under a stable leader.
The core of Paxos is not "the one with the most votes wins," but rather ensuring that new proposals preserve decisions that may have already been formed in the past as they proceed.
Roles and Terminology
Name
Meaning
Proposer
The role that selects a proposal number and initiates the protocol; not necessarily the client
Acceptor
The role that stores promise and accept records
Learner
The role that learns which value has been chosen
ballot / proposal number
Unique and totally ordered proposal number
accepted
An acceptor has accepted a proposal
chosen
The same proposal has been accepted by the required quorum
A single server can assume multiple roles. Here we use a majority quorum from a fixed set, discussing a model that is non-Byzantine and allows crash recovery. The protocol requires acceptors to retain necessary persistent state after restart.
Why acceptors cannot simply accept the first value they see
If three acceptors first receive X, Y, and Z respectively, and never accept any further proposals, no value will achieve a majority. To make progress, acceptors must allow accepting subsequent proposals with higher numbers.
However, "allowing higher numbers to overwrite" introduces safety issues: A and B accept X, then B and C accept Y. Although the two majorities clearly intersect, two different values are selected. If the intersection node B lacks a constrained history inheritance rule, it might still participate in two different decisions.
Therefore, two complementary mechanisms are needed: use Promises to restrict old proposals accepted in the future, and use accepted records to constrain the values new proposers can propose.
Phase 1: Prepare and Promise
The Proposer selects a new unique ballot n and sends Prepare(n). A common construction is the lexicographical order of (local incrementing counter, proposer_id), and numbers already used cannot be reused after restart.
If an Acceptor allows promise n, it persistently records its promise lower bound and replies with a Promise: it will not accept proposals below this lower bound in the future, and attaches the highest-numbered proposal (accepted_ballot, accepted_value) accepted previously, or returns empty if none.
After receiving a majority of Promises, the Proposer examines these replies: if there are accepted records, it selects the value from the one with the largest accepted_ballot; if all are empty, it can choose its own new value. It is not arbitrary selection of any returned value, nor is it choosing the value that appears most frequently.
Phase 2: Accept and Accepted
The Proposer sends Accept(n,v). The Acceptor checks that it has not promised a higher ballot, persistently saves the accept record, and returns Accepted. If it has already promised a higher ballot, it rejects the old request.
When the same (n,v) is accepted by a majority, v is already chosen, even if the proposer crashes before collecting all responses. The Learner learning the decision is a separate event; "no one received a successful response" does not equal "no decision was formed."
Implementations usually advance the promise lower bound at the time of acceptance to the corresponding ballot and handle idempotent retries for messages with the same number. For the same instance, the same ballot cannot be used by a proposer to propose two different values.
A complete inheritance example
Assume X was accepted by A and B in ballot 4. A new proposer contacts B and C with ballot 7. B reports (4,X), C reports empty; the new proposer must therefore propose X. If the return contains a higher accepted_ballot, that value should be inherited; the inductive property of the protocol ensures it will not violate earlier chosen values either.
Conversely, if X in ballot 4 only reached A, and ballot 7 obtained empty Promises from B and C, Y can be chosen. B and C will no longer accept ballot 4, so the old proposer cannot make X achieve a majority anymore. This shows that Phase 1 not only collects the past but also constrains what can happen in the future through promises.
These examples illustrate two directions of safety: already chosen values cannot be swapped, and unchosen low-number competitors cannot bypass new majority promises to form conflicting decisions. For a complete derivation, see Paxos Made Simple.
Running two message traces: Chosen vs. Single-point Acceptance
Below, message delivery is written as sequential calls, and the two fields of the acceptor represent the state that should be persisted. Ballots are simplified to globally unique integers; Prepare with the same ballot can be replied to idempotently, and swapping values with the same ballot is explicitly rejected. The code does not implement networking, persistence, or fault recovery.
classAcceptor:def__init__(self):self.promised=-1self.accepted=Nonedefprepare(self,ballot):ifballot<self.promised:returnFalse, Noneself.promised=ballotreturnTrue, self.accepteddefaccept(self,ballot,value):ifballot<self.promised:returnFalseifself.acceptedandself.accepted[0]==ballotandself.accepted[1]!=value:raiseValueError("Cannot change value for the same ballot")self.promised=ballotself.accepted=(ballot,value)returnTruedefphase1(cluster,names,ballot,proposed):replies=[cluster[name].prepare(ballot)fornameinsorted(set(names))]promises=[acceptedforok,acceptedinrepliesifok]iflen(promises)<len(cluster)//2+1:raiseRuntimeError("Did not obtain majority Promise")history=[acceptedforacceptedinpromisesifacceptedisnotNone]returnmax(history,key=lambdaaccepted:accepted[0])[1]ifhistoryelseproposed# Trace 1: A, B already accepted X. Whether responses reach the proposer does not affect the fact that it is chosen.
c={name:Acceptor()fornamein"ABC"}assertphase1(c,"AB",4,"X")=="X"assertc["A"].accept(4,"X")andc["B"].accept(4,"X")assertsum(a.accepted==(4,"X")forainc.values())==2chosen_for_7=phase1(c,"BC",7,"Y")assertchosen_for_7=="X"assertc["B"].accept(7,chosen_for_7)andc["C"].accept(7,chosen_for_7)assertnotc["B"].accept(4,"X")# Trace 2: 4/X only reached A. B promised 4, but has no accepted record.
c={name:Acceptor()fornamein"ABC"}assertphase1(c,"AB",4,"X")=="X"assertc["A"].accept(4,"X")chosen_for_7=phase1(c,"BC",7,"Y")assertchosen_for_7=="Y"assertc["B"].accept(7,"Y")andc["C"].accept(7,"Y")assertnotc["B"].accept(4,"X")andnotc["C"].accept(4,"X")assertc["A"].accepted==(4,"X")# Local old accepted records can temporarily remain
In Trace 2, B replying to Promise(4) does not mean it accepted X. Phase 1 must return the accepted history; it cannot merge "the largest promised ballot" and "the largest accepted ballot/value" into a single field. The former restricts subsequent requests, while the latter constrains the values of new proposals.
Conversely, in Trace 1, B providing only a record of X is sufficient to constrain the new proposer; there is no need to vote again in B and C's replies to gather a "majority of replies saying X". B might be the only intersection between the new quorum and the old chosen quorum.
This model assumes the proposer complies with the selection returned by phase1, and globally a ballot is not assigned to two proposers. The acceptor's local same-ballot check can only detect conflicts it has seen, and cannot prevent a malicious proposer from lying to different acceptors about different values; Paxos's fault model is not a Byzantine model. State loss or illegal proposer behavior is not within the premises of this safety reasoning. Paxos Made Simple
Why can't new proposals arbitrarily change values?
Seeing only one X in the new Promise set might still require inheriting X: this reply might be the exact intersection with the old chosen majority. Below, we distinguish between two initial histories: "only A accepted" and "A, B already chosen," and retain the late request for old ballot 4. This allows us to see both how Phase 1 discovers the past and how it blocks future conflict paths through Promises.
Preparing the visual
Why can't new proposals arbitrarily change values?
Promises restrict future acceptable ballots, accepted saves past accepted values. Phase 1 selects the value with the highest accepted ballot, not voting by the number of returned values.
{"id":"paxos-inherited-value","title":"Why can't new proposals arbitrarily change values?","summary":"Promises restrict future acceptable ballots, accepted saves past accepted values. Phase 1 selects the value with the highest accepted ballot, not voting by the number of returned values.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"seed\"><button data-mode=\"one\" data-i18n=\"one\"></button><button data-mode=\"two\" data-i18n=\"two\"></button></div><div class=\"acceptors\"><section class=\"card\"><header>A</header><code id=\"state0\"></code><button id=\"prepare0\" data-i18n=\"prepare\"></button><button id=\"accept0\" data-i18n=\"accept\"></button><button id=\"old0\" data-i18n=\"old\"></button></section><section class=\"card\"><header>B</header><code id=\"state1\"></code><button id=\"prepare1\" data-i18n=\"prepare\"></button><button id=\"accept1\" data-i18n=\"accept\"></button><button id=\"old1\" data-i18n=\"old\"></button></section><section class=\"card\"><header>C</header><code id=\"state2\"></code><button id=\"prepare2\" data-i18n=\"prepare\"></button><button id=\"accept2\" data-i18n=\"accept\"></button><button id=\"old2\" data-i18n=\"old\"></button></section></div><div class=\"metrics\"><div><span data-i18n=\"proposal\"></span><strong id=\"proposal\"></strong></div><div><span data-i18n=\"chosen\"></span><strong id=\"chosen\"></strong></div></div><p id=\"feedback\" class=\"status\" role=\"status\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n\n.acceptors{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:16px 0}.card code{display:block;white-space:pre-line;min-height:9em}.card button{margin-top:8px;padding:9px 3px}.metrics strong{font-size:18px}.metrics strong{min-height:6em}.status{min-height:7em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let states,replies,value,seed='two',chosen;function reset(){states=[{p:4,a:[4,'X']},{p:4,a:seed==='two'?[4,'X']:null},{p:0,a:null}];replies=new Map;value=null;chosen=seed==='two'?'X':null;set('feedback',t('collect'));draw();}function draw(){states.forEach((s,i)=>{set('state'+i,'promise '+s.p+'\\naccepted '+(s.a?s.a.join(':'):t('none')));$('#prepare'+i).disabled=replies.has(i)||replies.size===2;$('#accept'+i).disabled=!value;});set('proposal',value||'—');set('chosen',chosen||t('pending'));pressed(seed);}for(let i=0;i<3;i++){$('#prepare'+i).onclick=()=>{states[i].p=7;replies.set(i,states[i].a?[...states[i].a]:null);if(replies.size===2){const history=[...replies.values()].filter(Boolean).sort((a,b)=>b[0]-a[0]);value=history.length?history[0][1]:'Y';}set('feedback','Promise 7: '+'ABC'[i]);draw();};for(const kind of ['accept','old'])$('#'+kind+i).onclick=()=>{const ballot=kind==='old'?4:7,v=kind==='old'?'X':value;if(!v)return;if(ballot<states[i].p)set('feedback',t('rejected'));else{states[i]={p:ballot,a:[ballot,v]};set('feedback',t('accepted'));if(states.filter(s=>s.a&&s.a[0]===ballot&&s.a[1]===v).length>=2)chosen=v;}draw();};}document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{seed=b.dataset.mode;reset();});reset();","audio":false,"strings":{"accept":"Accept 7","accepted":"Request accepted","chosen":"Chosen value","collect":"Still need to collect two Promises","heading":"Why can't new proposals arbitrarily change values?","intro":"First select the history of ballot 4, then send Prepare(7) to any node. After collecting two Promises, inherit the value with the highest accepted ballot.","none":"Not yet accepted","old":"Late Accept 4","one":"Only A accepted X","pending":"No majority evidence yet","prepare":"Prepare 7","proposal":"Value for ballot 7","rejected":"Old ballot rejected by Promise","reset":"Reset","scope":"Single slot, three protocol-compliant acceptors, unique ballot. Old message for 4 only carries X; 7 collects exactly two Promises to fix the proposal. State is considered persistent, crashes are not simulated.","scopeLabel":"Model Scope and Assumptions","seed":"Initial accepted history","two":"A, B already chosen X"}}
Safety does not necessarily mean fast value selection
Two proposers might continuously increase ballots, interrupting each other, keeping the system safe but unable to complete for a long time. Therefore, implementations usually rely on a stable leader, backoff, or improved communication and scheduling conditions for eventual stability to make progress.
Simply retrying infinitely after receiving a rejection for a higher ballot does not guarantee low latency; it is necessary to avoid storming competition during fault recovery. When a majority of nodes are unreachable, the basic majority scheme cannot continue to select new values, but historical inheritance rules should not be abandoned because of this.
Multi-Paxos: Using Phase 1 results for multiple slots
Log slot and ballot are two dimensions: slot refers to the position of the command in the log, and ballot indicates who participates in the competition with which proposal generation. "New ballot" cannot be understood as "the next log entry."
After a stable leader establishes valid Phase 1 promises for relevant slots, it can execute Phase 2 for subsequent slots using the same ballot within the range covered by these promises, amortizing the Prepare cost. It is not because "I think I am still the leader" that recovery is arbitrarily skipped: the new leader must collect acceptance history, preserve constrained values slot by slot, and then handle missing slots.
Normally, each batch of proposals can use one round of accept requests and responses, but client communication, persistence, queues, and state machine application still exist. Multiple slots may reach decisions in parallel, but the state machine usually still needs to apply continuous prefixes; it cannot skip undetermined slot 11 to execute dependent commands just because slot 12 finished first.
Slot 12 being chosen first does not mean it can be executed first
Assume the initial state is 10, the command for slot 10 is add 2, slot 11 is subtract 1, and slot 12 is multiply by 3. 10 and 12 are known first, 11 is temporarily missing. The state machine can only execute 10 first, then stop at 11; otherwise, multiplying first then subtracting would yield the wrong 35, while the correct log order yields 33.
"11 is unknown" does not mean "11 can be arbitrarily filled with no-op." The new leader still needs to collect the acceptance history for that slot through the recovery protocol, respecting constraints on potentially existing values; only when the protocol allows choosing a new value can a no-op be used to fill the empty slot. Log replication services need to record the set of chosen values and the applicable prefix separately; the known maximum slot cannot replace execution progress.
What is missing from algorithm to service
Production implementations also require log truncation and snapshots, membership changes, client request deduplication, flow control, read semantics, and operational recovery. These are not "Paxos does not allow," but rather the single-value protocol did not make unique engineering choices for the entire system.
Raft organizes election eligibility and log prefix constraints into a more explicit protocol structure; the implementation space for Multi-Paxos elections and log recovery is larger. Comparisons should focus on specific implementations and guarantees, rather than asserting that all replicas of a certain protocol always have exactly the same logs.