Consensus Protocols in Distributed Systems: Paxos, Raft, and Byzantine Fault Tolerance

Meta Description: Understand distributed consensus protocols. Learn how Raft, Paxos, and PBFT establish state machine replication and tolerate node failures.

┌────────────────────────────────────────────────────────────────────────┐

│                   THE DISTRIBUTED CONSENSUS PROBLEM                    │

│                                                                        │

│   Client Write Request: “Set Key X = 42”                               │

│            │                                                           │

│            ▼                                                           │

│   ┌─────────────────┐    Consensus     ┌─────────────────┐             │

│   │ Node A (Leader) ├─────────────────►│ Node B (Follower)             │

│   └────────┬────────┘    Replication   └─────────────────┘             │

│            │                           (Network Partition / Crash?)    │

│            ▼                                                           │

│   ┌─────────────────┐                  ┌─────────────────┐             │

│   │ Node C (Follower)│                  │ Node D (Offline)│             │

│   └─────────────────┘                  └─────────────────┘             │

│                                                                        │

│   Goal: All operational nodes commit the EXACT same log entry sequence. │

└────────────────────────────────────────────────────────────────────────┘

Building scalable microservices often requires distributing software across multiple independent server nodes. While single-node databases rely on local storage engines to manage transactions, distributed systems must maintain state consistency across networks prone to packet loss, variable latencies, and hardware failures.

This problem is known as Distributed Consensus: how can a cluster of independent, untrusted, or crash-prone nodes agree on a single data value, transaction order, or state change?

Consensus algorithms form the foundation of modern cloud-native systems. Technologies like Kubernetes (etcd), Apache ZooKeeper, HashiCorp Consul, and distributed SQL databases (CockroachDB, YugabyteDB) rely on consensus protocols—such as Raft and Paxos—to guarantee state machine replication and high availability.

💡 Key Takeaways

  • State Machine Replication: Nodes execute identical sequences of state commands from a shared, immutable log.
  • The FLP Impossibility Result: In an asynchronous network, no deterministic consensus protocol can guarantee safety and liveness if even a single node can crash.
  • Crash Fault Tolerance (CFT): Protocols like Paxos and Raft tolerate up to $f$ node crashes in a cluster of $2f + 1$ nodes using majority quorums.
  • Byzantine Fault Tolerance (BFT): Protocols like PBFT protect systems against malicious, arbitrary, or corrupted node behaviors, requiring $3f + 1$ total nodes to tolerate $f$ faulty nodes.

Fundamental Guarantees: Safety vs. Liveness

When designing or evaluating consensus algorithms, system architects evaluate two fundamental properties:

┌────────────────────────────────────────────────────────────────────────┐

│                        SAFETY VS. LIVENESS                             │

│                                                                        │

│   SAFETY (“Nothing bad happens”)                                       │

│   • Non-faulty nodes never commit different values for the same log   │

│     index.                                                             │

│   • A committed transaction state is permanent and linearizable.       │

│                                                                        │

│   LIVENESS (“Something good eventually happens”)                       │

│   • The system continues processing incoming client requests despite   │

│     partial node failures or temporary network partitions.             │

└────────────────────────────────────────────────────────────────────────┘

The FLP Impossibility Theorem (Fischer, Lynch, Paterson – 1985)

A core proof in distributed computing proves that in a fully asynchronous network, no deterministic consensus algorithm can guarantee both Safety and Liveness if even a single process can unannouncedly crash.

Because networks cannot reliably distinguish between a dead node and a slow network link, practical consensus protocols (like Raft and Paxos) prioritize Safety above all else. If a network partition occurs, the system preserves consistency by pausing writes on minority partitions until network connectivity recovers.

Paxos: The Pioneer of Consensus

Formulated by Leslie Lamport in 1989, Paxos was the first mathematically proven Crash Fault-Tolerant (CFT) consensus protocol.

┌────────────────────────────────────────────────────────────────────────┐

│                         BASIC PAXOS TWO-PHASE FLOW                     │

│                                                                        │

│   Proposer                     Acceptor 1    Acceptor 2    Acceptor 3  │

│      │                            │             │             │        │

│      ├─── Phase 1a: Prepare(n) ──►│             │             │        │

│      │◄── Phase 1b: Promise(n) ───┤             │             │        │

│      │                            │             │             │        │

│      ├─── Phase 2a: Accept(n, v)─►│             │             │        │

│      │◄── Phase 2b: Accepted(n, v)┤             │             │        │

│      │                            │             │             │        │

│   (Consensus Reached: Value ‘v’ is committed across majority quorum)   │

└────────────────────────────────────────────────────────────────────────┘

Basic Paxos Phases

  1. Phase 1 (Prepare):
    1. A Proposer selects a unique proposal number $n$ and broadcasts a Prepare(n) message to a majority of Acceptors.
    1. An Acceptor accepts Prepare(n) if $n$ is higher than any proposal number it has previously seen, returning a Promise not to accept future proposals numbered less than $n$.
  2. Phase 2 (Accept):
    1. Once the Proposer receives promises from a majority of Acceptors, it sends an Accept(n, v) message containing the proposed value $v$.
    1. Acceptors log and register the value $v$ unless they have already responded to a higher proposal number $n’ > n$.

Multi-Paxos

Basic Paxos only agrees on a single value. To manage persistent application states, systems use Multi-Paxos, which elects a stable leader to streamline Phase 1 across continuous streams of log entries. However, Multi-Paxos is notoriously difficult to implement correctly, which led to the creation of Raft.

Raft: Designed for Understandability and Operational Clarity

Introduced by Ongaro and Ousterhout in 2014, Raft decomposes distributed consensus into three distinct sub-problems: Leader Election, Log Replication, and Safety.

┌────────────────────────────────────────────────────────────────────────┐

│                        RAFT NODE STATE TRANSITIONS                     │

│                                                                        │

│                     ┌──────────────────────────────┐                   │

│                     │                              │                   │

│                     ▼                              │ Times out,        │

│            ┌─────────────────┐  Starts Election    │ starts new        │

│            │    Follower     ├─────────────────┐   │ election          │

│            └────────┬────────┘                 │   │                   │

│                     ▲                          ▼   │                   │

│      Discovers      │                  ┌───────────┴─────┐             │

│      Leader or      │                  │    Candidate    │             │

│      Higher Term    │                  └───────┬─────────┘             │

│                     │                          │ Receives Votes        │

│                     │                          │ from Majority         │

│                     │                          ▼                       │

│                     │                  ┌─────────────────┐             │

│                     └──────────────────┤     Leader      │             │

│                                        └─────────────────┘             │

└────────────────────────────────────────────────────────────────────────┘

Raft Protocol Invariants

  1. Strong Leader: At any given time (called a Term), exactly one node acts as Leader. The Leader receives all incoming client write requests, appends them to its local log, and manages entry replication across Followers.
  2. Randomized Election Timers: Followers convert to Candidates if they do not receive heartbeats within a randomized window (e.g., 150ms–300ms). This randomization prevents split-vote deadlocks during leader elections.
  3. Majority Quorum Replication: A log entry is considered Committed once it has been successfully written to disk by a majority of nodes ($\lfloor N/2 \rfloor + 1$).

Hands-On Implementation: Building a Raft Leader Election State Machine in Python

Let’s implement a simplified, working simulation of the Raft Leader Election and Heartbeat Mechanism in Python using asynchronous event handling.

Python

import asyncio

import random

import time

from enum import Enum

from typing import List, Dict

class NodeRole(Enum):

    FOLLOWER = “Follower”

    CANDIDATE = “Candidate”

    LEADER = “Leader”

class RaftNode:

    def __init__(self, node_id: int, peers: List[int]):

        self.node_id = node_id

        self.peers = peers

        self.role = NodeRole.FOLLOWER

        self.current_term = 0

        self.voted_for = None

        self.votes_received = 0

        # Heartbeat and election timers (in seconds)

        self.last_heartbeat_time = time.time()

        self.election_timeout = random.uniform(1.5, 3.0)

        self.is_running = True

    async def start(self, cluster: Dict[int, ‘RaftNode’]):

        “””Main lifecycle loop for the Raft node.”””

        asyncio.create_task(self._run_election_timer(cluster))

        asyncio.create_task(self._run_leader_heartbeat(cluster))

    async def _run_election_timer(self, cluster: Dict[int, ‘RaftNode’]):

        “””Monitors heartbeats and triggers elections on timeout.”””

        while self.is_running:

            await asyncio.sleep(0.1)

            elapsed = time.time() – self.last_heartbeat_time

            if self.role != NodeRole.LEADER and elapsed >= self.election_timeout:

                await self._start_election(cluster)

    async def _start_election(self, cluster: Dict[int, ‘RaftNode’]):

        “””Transitions node to Candidate state and requests votes from cluster peers.”””

        self.role = NodeRole.CANDIDATE

        self.current_term += 1

        self.voted_for = self.node_id

        self.votes_received = 1  # Vote for self

        self.last_heartbeat_time = time.time()

        self.election_timeout = random.uniform(1.5, 3.0)

        print(f”[TERM {self.current_term}] Node {self.node_id} timed out. Starting election as CANDIDATE…”)

        # Request votes from peer nodes

        for peer_id in self.peers:

            if peer_id in cluster:

                asyncio.create_task(cluster[peer_id].request_vote(self.current_term, self.node_id, self, cluster))

    async def request_vote(self, term: int, candidate_id: int, candidate_ref: ‘RaftNode’, cluster: Dict[int, ‘RaftNode’]):

        “””RPC Endpoint: Processes RequestVote calls from Candidate nodes.”””

        if term > self.current_term:

            self.current_term = term

            self.role = NodeRole.FOLLOWER

            self.voted_for = None

        # Vote YES if candidate’s term is current and node hasn’t voted yet this term

        if term == self.current_term and (self.voted_for is None or self.voted_for == candidate_id):

            self.voted_for = candidate_id

            self.last_heartbeat_time = time.time()

            print(f”  └─ Node {self.node_id} granted vote to Candidate {candidate_id} for Term {term}.”)

            await candidate_ref.receive_vote_response(True, cluster)

        else:

            await candidate_ref.receive_vote_response(False, cluster)

    async def receive_vote_response(self, vote_granted: bool, cluster: Dict[int, ‘RaftNode’]):

        “””Tracks incoming election votes and transitions to Leader upon majority.”””

        if self.role == NodeRole.CANDIDATE and vote_granted:

            self.votes_received += 1

            majority = (len(self.peers) + 1) // 2 + 1

            if self.votes_received >= majority:

                self.role = NodeRole.LEADER

                print(f”\n[ELECTION SUCCESS] Node {self.node_id} secured majority ({self.votes_received} votes). Promoted to LEADER for Term {self.current_term}!\n”)

    async def _run_leader_heartbeat(self, cluster: Dict[int, ‘RaftNode’]):

        “””Leader loop: Sends periodic AppendEntries heartbeats to suppress new elections.”””

        while self.is_running:

            await asyncio.sleep(0.5)

            if self.role == NodeRole.LEADER:

                for peer_id in self.peers:

                    if peer_id in cluster:

                        cluster[peer_id].receive_heartbeat(self.node_id, self.current_term)

    def receive_heartbeat(self, leader_id: int, term: int):

        “””RPC Endpoint: Resets election timer upon receiving valid Leader heartbeat.”””

        if term >= self.current_term:

            self.current_term = term

            self.role = NodeRole.FOLLOWER

            self.last_heartbeat_time = time.time()

if __name__ == “__main__”:

    async def run_simulation():

        print(“— STARTING 3-NODE RAFT CONSENSUS SIMULATION —\n”)

        nodes_map = {}

        node_ids = [1, 2, 3]

        for nid in node_ids:

            peers = [p for p in node_ids if p != nid]

            nodes_map[nid] = RaftNode(node_id=nid, peers=peers)

        # Launch all node lifecycle tasks

        for nid in node_ids:

            await nodes_map[nid].start(nodes_map)

        # Let simulation execute through an election cycle

        await asyncio.sleep(4.0)

        # Terminate simulation

        for nid in node_ids:

            nodes_map[nid].is_running = False

        print(“— SIMULATION COMPLETE —“)

    asyncio.run(run_simulation())

Byzantine Fault Tolerance (BFT): Consensus Under Adversarial Conditions

Crash Fault-Tolerant (CFT) protocols like Paxos and Raft assume that while nodes can crash or drop messages, they are never malicious, nor do they send corrupted data.

In untrusted network environments—such as public blockchains, peer-to-peer networks, or multi-tenant cloud ecosystems—nodes can fail arbitrarily, lie, or transmit conflicting messages to different peers. This is known as the Byzantine Generals Problem.

┌────────────────────────────────────────────────────────────────────────┐

│                        CFT VS. BFT THREAT MODELS                       │

│                                                                        │

│   CRASH FAULT TOLERANCE (CFT) – Raft / Paxos                           │

│   • Threat Model : Nodes fail by crashing, pausing, or dropping pkts.  │

│   • Node Honesty : Assumed 100% honest (Non-Byzantine).                │

│   • Quorum Size  : Requires N = 2f + 1 total nodes (Tolerates f crashes)│

│                                                                        │

│   BYZANTINE FAULT TOLERANCE (BFT) – PBFT / Tendermint                  │

│   • Threat Model : Nodes may crash, forge signatures, lie, or cheat.   │

│   • Node Honesty : Up to ‘f’ nodes can be actively malicious.          │

│   • Quorum Size  : Requires N = 3f + 1 total nodes (Tolerates f bad)   │

└────────────────────────────────────────────────────────────────────────┘

Practical Byzantine Fault Tolerance (PBFT)

Introduced by Castro and Liskov in 1999, PBFT proved that state machine replication can operate safely in asynchronous networks containing malicious actors, provided that at least $2f + 1$ nodes out of a total $N = 3f + 1$ nodes remain honest.

PBFT processes consensus across three sequential phases: Pre-Prepare, Prepare, and Commit, using cryptographic signatures to verify message authenticity across all nodes.

Comparing Consensus Protocols

Engineers choose consensus implementations based on their deployment environment and fault models:

ProtocolFault ModelQuorum RequirementLeader ModelPrimary Use Case
PaxosCrash Faults (CFT)$N = 2f + 1$Dual / Multi-ProposerGoogle Spanner, Chubby Lock Service
RaftCrash Faults (CFT)$N = 2f + 1$Single Strong LeaderKubernetes (etcd), HashiCorp Consul, CockroachDB
PBFTByzantine (BFT)$N = 3f + 1$Primary / ReplicaPrivate Enterprise Blockchains (Hyperledger)
TendermintByzantine (BFT)$N = 3f + 1$Deterministic Round-Robin LeaderCosmos SDK, Distributed Proof-of-Stake Networks

Frequently Asked Questions (FAQ)

What happens in Raft during a split-brain scenario caused by a network partition?

If a 5-node cluster is partitioned into two networks ($A, B$ with 3 nodes, and $C, D, E$ with 2 nodes), the 3-node partition maintains a majority quorum ($\lfloor 5/2 \rfloor + 1 = 3$). It can continue to elect a leader and commit write operations. The 2-node partition cannot form a majority, so it rejects writes. When the network heals, nodes in the minority partition recognize the higher term count of the majority leader and roll back uncommitted entries to reconcile state seamlessly.

Why do CFT algorithms require $2f + 1$ nodes, while BFT algorithms require $3f + 1$ nodes?

  • In CFT: A node can only fail by stopping. In a cluster of $2f + 1$ nodes, receiving responses from $f + 1$ nodes guarantees a majority quorum, ensuring at least one node holds the latest log state.
  • In BFT: Malicious nodes can actively send false information or vote for conflicting states. To prevent $f$ malicious nodes from colluding with $f$ slow or unresponsive honest nodes, the system requires a super-majority of $2f + 1$ matching responses out of $N = 3f + 1$ total nodes.

Is consensus required for every read operation in distributed databases?

Not necessarily. While writing data requires consensus replication to maintain safety, databases optimize reads using mechanisms like Read Indexes or Lease Reads. In Lease Reads, the Raft leader serves local read queries directly without quorum network roundtrips, provided its time-bound leader lease remains valid.

Conclusion & Action Steps

Consensus protocols turn clusters of independent hardware nodes into cohesive, reliable systems. Understanding how Raft handles leader elections, Paxos coordinates proposals, and BFT guards against untrusted inputs helps engineers build resilient distributed systems.

Next Steps for Systems Architects:

  1. Use etcdctl or consul locally to inspect how leader elections and key-value entries are replicated across nodes.
  2. Read the seminal research paper “In Search of an Understandable Consensus Algorithm” by Diego Ongaro and John Ousterhout.
  3. Simulate node failures, delays, and network partitions within your system’s integration tests to verify quorum behaviors under stress.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *