Paxos Consensus Algorithm

What is Paxos?

Paxos is a family of protocols for achieving consensus in a distributed system of unreliable nodes. It ensures that multiple nodes agree on a single value, even when some nodes fail.

Developed by: Leslie Lamport (1989, published 1998)

Key Property: Safety - never chooses conflicting values

Fault Tolerance: Tolerates f failures with 2f + 1 nodes

Paxos Architecture

graph LR subgraph Proposers["PROPOSERS"] P1["Proposer 1
Proposes: value_A
Proposal#: 1"] P2["Proposer 2
Proposes: value_B
Proposal#: 2"] end subgraph Acceptors["ACCEPTORS (Voters)"] A1["Acceptor 1"] A2["Acceptor 2"] A3["Acceptor 3"] A4["Acceptor 4"] A5["Acceptor 5"] end subgraph Learners["LEARNERS"] L1["Learner 1
Learns chosen value"] L2["Learner 2
Learns chosen value"] end P1 -.->|"PREPARE"| A1 P1 -.->|"PREPARE"| A2 P1 -.->|"PREPARE"| A3 A1 -.->|"PROMISE"| P1 A2 -.->|"PROMISE"| P1 A3 -.->|"PROMISE"| P1 P1 -->|"ACCEPT"| A1 P1 -->|"ACCEPT"| A2 P1 -->|"ACCEPT"| A3 A1 -->|"ACCEPTED"| L1 A2 -->|"ACCEPTED"| L1 A3 -->|"ACCEPTED"| L2 style Proposers fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#2e3440 style Acceptors fill:#fff3e0,stroke:#f57c00,stroke-width:2px,color:#2e3440 style Learners fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#2e3440

Three Roles in Paxos

Role Responsibilities State
Proposer • Proposes values to be agreed upon
• Generates unique proposal numbers
• Sends PREPARE and ACCEPT messages
• Proposal number (unique, increasing)
• Proposed value
• Set of promises/accepts received
Acceptor • Votes on proposals
• Promises to ignore lower-numbered proposals
• Accepts proposals that meet conditions
• Highest promised proposal number
• Highest accepted proposal
Learner • Receives ACCEPTED messages
• Determines when consensus is reached
• Learns the chosen value
• Tracking which proposals were accepted
• The learned consensus value

Phase 1: Prepare/Promise

sequenceDiagram participant P as Proposer participant A1 as Acceptor 1 participant A2 as Acceptor 2 participant A3 as Acceptor 3 Note over P: Generate proposal number: 5
Value: "value_A" P->>+A1: PREPARE(n=5) Note over A1: Check: 5 > promised?
promised=None ✓ A1->>A1: promised = 5 A1-->>-P: PROMISE(n=5, accepted=None) P->>+A2: PREPARE(n=5) Note over A2: Check: 5 > promised?
promised=None ✓ A2->>A2: promised = 5 A2-->>-P: PROMISE(n=5, accepted=None) P->>+A3: PREPARE(n=5) Note over A3: Check: 5 > promised?
promised=None ✓ A3->>A3: promised = 5 A3-->>-P: PROMISE(n=5, accepted=None) Note over P: Received 3/3 PROMISES
✓ Majority reached (need 2) rect rgb(163, 190, 140, 0.2) Note over P,A3: Phase 1 Complete
Proposer has permission to propose end

Phase 1a: Prepare (Proposer → Acceptors)

Proposer sends: PREPARE(proposal_number)

Purpose: Ask acceptors if they will accept this proposal

Proposal number must be: Unique and higher than any previous

Phase 1b: Promise (Acceptors → Proposer)

Acceptor responds with PROMISE if:

Promise includes:

Acceptor responds with NACK if:

Phase 2: Accept/Accepted

sequenceDiagram participant P as Proposer participant A1 as Acceptor 1 participant A2 as Acceptor 2 participant A3 as Acceptor 3 participant L as Learner Note over P: Received majority PROMISES
Use value from highest accepted
or own value if none P->>+A1: ACCEPT(n=5, value="value_A") Note over A1: Check: 5 >= promised?
promised=5 ✓ A1->>A1: accepted = (5, "value_A") A1-->>-P: ACCEPTED(n=5, value="value_A") A1->>L: Notify: ACCEPTED(n=5, value="value_A") P->>+A2: ACCEPT(n=5, value="value_A") Note over A2: Check: 5 >= promised?
promised=5 ✓ A2->>A2: accepted = (5, "value_A") A2-->>-P: ACCEPTED(n=5, value="value_A") A2->>L: Notify: ACCEPTED(n=5, value="value_A") P->>+A3: ACCEPT(n=5, value="value_A") Note over A3: Check: 5 >= promised?
promised=5 ✓ A3->>A3: accepted = (5, "value_A") A3-->>-P: ACCEPTED(n=5, value="value_A") A3->>L: Notify: ACCEPTED(n=5, value="value_A") Note over P: Received 3/3 ACCEPTS
✓ Consensus reached! Note over L: Received 3 ACCEPTED messages
✓ Majority accepted "value_A"
LEARNED: "value_A" rect rgb(163, 190, 140, 0.2) Note over P,L: Phase 2 Complete
Value "value_A" is CHOSEN end

Phase 2a: Accept (Proposer → Acceptors)

Proposer sends: ACCEPT(proposal_number, value)

Value must be:

Phase 2b: Accepted (Acceptors → Proposer & Learners)

Acceptor responds with ACCEPTED if:

Acceptor updates state:

Consensus is reached when:

Complete 2-Phase Protocol Flow

graph TB Start([Client: Propose Value]) subgraph Phase1["PHASE 1: PREPARE / PROMISE"] P1A["Proposer: Generate
unique proposal number n"] P1B["Proposer: Send PREPARE(n)
to all acceptors"] A1A["Acceptors: Receive PREPARE(n)"] A1B{"n > highest
promised?"} A1C["Acceptor: Update promised = n
Send PROMISE(n, accepted)"] A1D["Acceptor: Send NACK"] P1C{"Majority
PROMISES?"} P1D["✗ ABORT:
Did not get majority"] end subgraph Phase2["PHASE 2: ACCEPT / ACCEPTED"] P2A["Proposer: Choose value
(from highest accepted or own)"] P2B["Proposer: Send ACCEPT(n, value)
to all acceptors"] A2A["Acceptors: Receive ACCEPT(n, value)"] A2B{"n >= highest
promised?"} A2C["Acceptor: accepted = (n, value)
Send ACCEPTED(n, value)"] A2D["Acceptor: Send NACK"] P2C{"Majority
ACCEPTED?"} P2D["✗ ABORT:
Did not get majority"] end Success([✓ Value is CHOSEN
Learners learn value]) Start --> P1A P1A --> P1B P1B --> A1A A1A --> A1B A1B -->|"Yes"| A1C A1B -->|"No"| A1D A1C --> P1C A1D --> P1C P1C -->|"Yes"| P2A P1C -->|"No"| P1D P2A --> P2B P2B --> A2A A2A --> A2B A2B -->|"Yes"| A2C A2B -->|"No"| A2D A2C --> P2C A2D --> P2C P2C -->|"Yes"| Success P2C -->|"No"| P2D style Phase1 fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#2e3440 style Phase2 fill:#fff3e0,stroke:#ef6c00,stroke-width:2px,color:#2e3440 style Success fill:#c8e6c9,stroke:#388e3c,stroke-width:3px,color:#2e3440 style P1D fill:#ffcdd2,stroke:#c62828,stroke-width:2px,color:#2e3440 style P2D fill:#ffcdd2,stroke:#c62828,stroke-width:2px,color:#2e3440

Competing Proposers Scenario

sequenceDiagram participant P1 as Proposer 1
(value_A) participant P2 as Proposer 2
(value_B) participant A as Acceptors Note over P1,A: Proposer 1 starts first P1->>A: PREPARE(n=1) A-->>P1: PROMISE(n=1) Note over P1,P2: Before P1 can send ACCEPT,
P2 interrupts with higher n P2->>A: PREPARE(n=5) Note over A: 5 > 1, update promised=5 A-->>P2: PROMISE(n=5, accepted=None) Note over P1: P1 tries to continue P1->>A: ACCEPT(n=1, value_A) Note over A: 1 < promised(5) ✗ A-->>P1: NACK (promised higher proposal) Note over P1: P1 FAILS P2->>A: ACCEPT(n=5, value_B) Note over A: 5 >= promised(5) ✓ A->>A: accepted = (5, value_B) A-->>P2: ACCEPTED(n=5, value_B) rect rgb(163, 190, 140, 0.2) Note over P2,A: P2 SUCCEEDS
Consensus: value_B end

Livelock Problem

Issue: Competing proposers can interrupt each other indefinitely

Example:

Solution: Multi-Paxos with leader election (one proposer at a time)

Properties & Guarantees

Property Guarantee Explanation
Safety Only one value can be chosen Never produces conflicting results, even with failures
Fault Tolerance Tolerates f failures with 2f+1 nodes With 5 acceptors, can tolerate 2 failures
Termination Not guaranteed May not reach consensus if proposers keep interrupting
Message Complexity O(n²) n acceptors × multiple phases
Latency 2 round trips Phase 1 (prepare) + Phase 2 (accept)

Python Implementation

Acceptor Class

class Acceptor:
    """Acceptor role in Paxos - votes on proposals

    State:
    - promised_proposal: Highest proposal number it has promised to consider
    - accepted_proposal: Highest proposal it has accepted
    """

    def __init__(self, node_id: str):
        self.node_id = node_id
        self.promised_proposal: Optional[Proposal] = None
        self.accepted_proposal: Optional[Proposal] = None

    def receive_prepare(self, proposal: Proposal) -> Message:
        """Phase 1b: Respond to prepare request

        Rules:
        - If proposal number > any previously promised, send PROMISE
        - Include any previously accepted proposal
        - Otherwise send NACK
        """
        if self.promised_proposal is None or proposal.number > self.promised_proposal.number:
            # Promise to not accept proposals with lower numbers
            self.promised_proposal = proposal

            return Message(
                type=MessageType.PROMISE,
                from_node=self.node_id,
                to_node="proposer",
                proposal=proposal,
                accepted_proposal=self.accepted_proposal
            )
        else:
            # Already promised a higher proposal number
            return Message(
                type=MessageType.NACK,
                from_node=self.node_id,
                to_node="proposer",
                proposal=proposal,
                promised_proposal=self.promised_proposal
            )

    def receive_accept(self, proposal: Proposal) -> Message:
        """Phase 2b: Respond to accept request

        Rules:
        - If proposal number >= promised number, accept it
        - Otherwise send NACK
        """
        if self.promised_proposal is None or proposal.number >= self.promised_proposal.number:
            # Accept the proposal
            self.accepted_proposal = proposal
            self.promised_proposal = proposal

            return Message(
                type=MessageType.ACCEPTED,
                from_node=self.node_id,
                to_node="proposer",
                proposal=proposal
            )
        else:
            # Already promised a higher proposal number
            return Message(
                type=MessageType.NACK,
                from_node=self.node_id,
                to_node="proposer",
                proposal=proposal,
                promised_proposal=self.promised_proposal
            )

Proposer Class

class Proposer:
    """Proposer role in Paxos - proposes values"""

    def __init__(self, node_id: str, value: Any):
        self.node_id = node_id
        self.value = value
        self.proposal_number = 1
        self.promises_received: Set[str] = set()
        self.highest_accepted: Optional[Proposal] = None

    def prepare(self, acceptors: Dict[str, Acceptor]) -> bool:
        """Phase 1a: Send PREPARE to all acceptors

        Returns True if majority promised, False otherwise
        """
        self.proposal_number += len(acceptors)  # Ensure uniqueness
        proposal = Proposal(self.proposal_number, self.value)

        self.promises_received.clear()

        # Send PREPARE to all acceptors
        for acceptor_id, acceptor in acceptors.items():
            response = acceptor.receive_prepare(proposal)

            if response.type == MessageType.PROMISE:
                self.promises_received.add(acceptor_id)

                # Track highest accepted proposal from responses
                if response.accepted_proposal:
                    if self.highest_accepted is None or response.accepted_proposal > self.highest_accepted:
                        self.highest_accepted = response.accepted_proposal
                        # CRITICAL: Must use value from highest accepted proposal
                        self.value = self.highest_accepted.value

        # Check if we have majority
        majority = len(acceptors) // 2 + 1
        return len(self.promises_received) >= majority

    def accept(self, acceptors: Dict[str, Acceptor]) -> bool:
        """Phase 2a: Send ACCEPT to all acceptors

        Returns True if majority accepted, False otherwise
        """
        proposal = Proposal(self.proposal_number, self.value)
        self.accepts_received.clear()

        for acceptor_id, acceptor in acceptors.items():
            response = acceptor.receive_accept(proposal)

            if response.type == MessageType.ACCEPTED:
                self.accepts_received.add(acceptor_id)

        majority = len(acceptors) // 2 + 1
        return len(self.accepts_received) >= majority

    def propose(self, acceptors: Dict[str, Acceptor]) -> Optional[Any]:
        """Run full Paxos protocol"""
        # Phase 1: Prepare
        if not self.prepare(acceptors):
            return None  # Failed to get majority promises

        # Phase 2: Accept
        if not self.accept(acceptors):
            return None  # Failed to get majority accepts

        return self.value  # Success!

Learner Class

class Learner:
    """Learner role in Paxos - learns the chosen value"""

    def __init__(self, node_id: str):
        self.node_id = node_id
        self.accepted_proposals: Dict[int, Set[str]] = {}
        self.learned_value: Optional[Any] = None

    def receive_accepted(self, acceptor_id: str, proposal: Proposal, total_acceptors: int):
        """Process ACCEPTED message from an acceptor"""
        if proposal.number not in self.accepted_proposals:
            self.accepted_proposals[proposal.number] = set()

        self.accepted_proposals[proposal.number].add(acceptor_id)

        # Check if majority accepted this proposal
        majority = total_acceptors // 2 + 1
        if len(self.accepted_proposals[proposal.number]) >= majority:
            self.learned_value = proposal.value
            print(f"LEARNED: Value '{self.learned_value}' has been chosen!")

    def get_learned_value(self) -> Optional[Any]:
        """Get the learned consensus value"""
        return self.learned_value

Paxos vs Raft Comparison

Feature Paxos Raft
Understandability Complex, hard to grasp Designed for clarity
Leader No leader (Multi-Paxos adds one) Strong leader-based
Log Replication Not built-in (needs Multi-Paxos) Built-in, core feature
Variants Many (Multi-Paxos, Fast Paxos, EPaxos) Fewer variants, standardized
Implementation Harder to implement correctly Easier to implement
Used In Google Chubby, ZooKeeper, Spanner etcd, Consul, CockroachDB

Real-World Usage

Google Chubby

Purpose: Distributed lock service

Consensus: Multi-Paxos for leader election and state replication

Scale: Thousands of cells across Google infrastructure

Apache ZooKeeper

Purpose: Coordination service for distributed applications

Consensus: ZAB (ZooKeeper Atomic Broadcast) - similar to Multi-Paxos

Features: Leader election, configuration management, distributed synchronization

Google Spanner

Purpose: Globally distributed database

Consensus: Paxos for replication groups

Innovation: TrueTime API for global consistency

Interview Tips

Common Questions

Key Insights