Search Tech Journey

Find topics, journeys and posts

6-month learning plan73 / 130
back to blog
systemsintermediate 55m read

S073 · Consensus — Paxos & Raft Intuition

How a group of machines that can crash, lag, or lie by omission agree on the same value. The algorithm powering etcd, ZooKeeper, Kafka, CockroachDB, and every serious cluster's brain.

⚙️SystemsM08 · Distributed Systems· Session 073 of 130 90 min

🎯 Explain why consensus is hard, walk through Raft's leader election + log replication + safety without notes, and name three real systems whose lifeblood is a Raft cluster.

Why this session exists

Every distributed system eventually needs a group of computers to agree on one fact: "who's the leader?", "what's the next log index?", "which replica is up-to-date enough to serve reads?". That sounds trivial until you add crashes, network partitions, and asynchronous messages — and then you discover it is the hardest problem in the field. Consensus is the algorithm that makes it possible. Once you understand Raft you understand how Kubernetes' etcd stays consistent, how Kafka picks controllers, how CockroachDB commits, how ZooKeeper keeps the world's ZooKeepers happy. Miss this session and every future "how does X coordinate?" answer is hand-waving.

You will be able to
  • State the FLP impossibility result in one sentence and explain how Raft dodges it in practice.
  • Walk through Raft's three sub-protocols (leader election, log replication, safety) without notes.
  • Compare Paxos and Raft on 'ease of understanding', 'academic priority', and 'production adoption'.
  • Name at least four production systems whose brain is a Raft cluster and one whose brain is Paxos.
  • Diagnose a split-brain incident by asking 'did you have a quorum?'.

Prerequisites

  • S072 · Consistency models — consensus is the mechanism that delivers linearizability across nodes.
  • S068 · Replication basics — leaders and followers must already be familiar words.
  • S042 · Networking · TCP — you need to know that "the message was sent" is not the same as "the message was received".


(a) Intuition · 5 min

Five friends trying to pick a restaurant over a bad phone line
🌍 Real world

Five friends are on a group call trying to pick a restaurant. The phone line is bad — messages randomly drop, some people freeze for 30 seconds and rejoin. You want a rule that guarantees: if the group ever settles on a choice, everyone who eventually rejoins agrees on that same choice. No two subgroups can convince themselves the pick was different.

The trick is: nothing counts as "decided" until a majority has explicitly agreed. Any subgroup with fewer than a majority might have missed messages from the majority — so it must defer.

💻 Code world

Raft is that rule, formalised. One friend is designated the "leader" for a term. She proposes each next choice. It only becomes committed once she has heard "yes" from a majority of the group (including herself). If she disappears, whoever else notices first says "term is over, elect a new leader" and holds an election — again decided by majority.

The whole algorithm rests on one property: two majorities of the same group always overlap. So no split-brain can commit two different values in the same term.

The impossibility result you can't dodge — and how Raft dodges it anyway

FLP (Fischer, Lynch, Paterson · 1985)
  • In an asynchronous network (no bound on message delay) with even one crashed node, no deterministic consensus algorithm can guarantee termination.
  • Translation: you cannot promise 'we WILL agree in bounded time' when the network can be arbitrarily slow.
  • Raft's dodge: use randomised election timeouts. In practice the network is 'partially synchronous' — most messages arrive within, say, 50 ms — and randomisation breaks symmetry so elections finish quickly.
  • This is the difference between 'impossible in theory' and 'works fine in production every day'. Understand which one applies before you argue on Twitter.

A quick history — why Raft won even though Paxos was first

  1. 1989
    Paxos · Lamport
    First algorithm to prove consensus is achievable under partial synchrony. Notoriously hard to understand.
  2. 1998
    'The Part-Time Parliament' published
    Lamport's Paxos paper, written as a parable about a Greek island. Rejected years earlier for being too weird.
  3. 2006
    Chubby · Google
    First famous industrial use — a Paxos-backed lock service that underpins BigTable, GFS.
  4. 2010
    ZooKeeper (ZAB) released
    Yahoo ships a Paxos-variant coordination service. Powers HBase, Kafka, Hadoop.
  5. 2014
    Raft · Ongaro & Ousterhout
    Stanford paper: 'consensus, but explainable in a lecture'. Community immediately adopts it.
  6. 2015
    etcd + CoreOS + Kubernetes
    etcd (Raft) becomes the brain of Kubernetes. Raft is now the default choice for new systems.
  7. 2021
    Kafka KRaft mode
    Kafka replaces its ZooKeeper (ZAB) dependency with its own Raft implementation. Even old Paxos systems migrate.

(b) Visual walkthrough · 15 min

Raft's state machine at a glance

The commit path — how a client write becomes durable

Raft's three sub-protocols

1election
1 · Leader election

Followers wait for heartbeats. If none arrive within a randomised timeout (150-300 ms), they become candidates, increment term, and request votes. Whoever gets a majority becomes leader.

2replication
2 · Log replication

The leader appends each client command to its log, then sends AppendEntries to all followers. Once a majority (including leader) has stored the entry, it is 'committed' and applied to state machines.

3safety
3 · Safety

Two guarantees: (a) only a candidate whose log is at-least-as-up-to-date as a majority can win an election, (b) a leader never overwrites its own log entries. Together these prevent split-brain commits.

What lives at which layer

Raft in the stack of a real database

Client SDK
Sends requests to any node; if the node is a follower it forwards to the leader (or returns a redirect).
L7
Consensus module (Raft)
The 500-1000 line state machine implementing election + replication + safety. Idealised in the paper, gnarly in practice.
L6
Log
An append-only file per replica: [term, index, command]. This is the durable ground truth of 'what the cluster agreed'.
L5
State machine
The application-level thing being replicated: a KV store, a lock table, a config directory. Committed log entries are applied here in order.
L4
Snapshot
Periodic dump of state machine so the log can be truncated. Otherwise the log grows forever.
L3
Network / RPC
gRPC or protobuf calls between nodes for RequestVote + AppendEntries. Assumed unreliable; retries built in.
L2

Paxos vs Raft — same job, very different vibes

Paxos (1989)

The academic OG

  • Two roles: proposers and acceptors, no explicit leader
  • Multi-Paxos adds a leader as an optimisation
  • Written in a style even Lamport's colleagues struggled with
  • Still the choice in Google's Chubby, Spanner, Megastore
  • Better for weird topologies (Flexible Paxos, quorum systems)
Raft (2014)

Consensus with a manual

  • Explicit leader is central, never optional
  • Strong leader restriction: only leader appends to log
  • Log 'holes' forbidden; strict linear log
  • Now dominant: etcd, Consul, CockroachDB, TiKV, MongoDB (config), Kafka KRaft, Nomad, RethinkDB, InfluxDB Enterprise
  • ~1000 LoC to implement; Paxos triple that
ZAB (2007, ZooKeeper)

Paxos-cousin, still widely deployed

  • ZooKeeper Atomic Broadcast — bespoke consensus for total-order broadcast
  • Powers Kafka (pre-KRaft), HBase, Hadoop namenode failover
  • Higher operational overhead than etcd — hence the industry drift to Raft
  • Worth knowing for legacy systems, not for new designs

The mental model to hold


Common misconception
✗ What most people think

"Raft elects a leader, and the leader decides. So consensus is basically just leader election plus following orders."

✓ What is actually true

Leader election is the easy half and it is not even required for safety — Raft remains correct with no leader, it simply makes no progress. The hard part is guaranteeing that a new leader never loses a committed entry, across arbitrary crashes and partitions where several nodes may have believed themselves leader. That guarantee comes from the election restriction and the commit rules, not from having a leader at all.

Why the myth is so sticky

The myth is sticky because election is the visible, animated, easily-diagrammed part of every Raft explanation, and because "one node decides" is a satisfying resolution to the problem. It misleads because it suggests the difficulty is choosing, when the difficulty is that a leader can be deposed at any moment without knowing it — it may be partitioned away and still believe it leads. Every subtle Raft rule exists to make that situation safe.

Prove it to yourself

The question that separates understanding from familiarity:

A leader in term 5 is partitioned away.
The majority elects a new leader in term 6.
The old leader has not noticed and still accepts writes.

Why is this SAFE?

Because the old leader cannot COMMIT: commit requires
acknowledgement from a majority, and it cannot reach one.
Its writes stay uncommitted and are overwritten when it
rejoins and learns of term 6.

Safety comes from what commit requires, not from preventing two nodes believing they lead. Raft allows that situation and makes it harmless.

From first principles
Start with the question

Why must a quorum be a strict majority? Why not any fixed number of nodes, like two out of five?

  1. 1
    A decision is only safe if a later decision cannot contradict it, which requires the later decider to learn about the earlier decision.
    forced by · two contradictory committed values would break the single-value guarantee consensus exists to provide
  2. 2
    The only way to guarantee that learning is to require that any two decision-making groups share at least one member, who carries the knowledge from one to the other.
    forced by · nodes communicate only with those they can reach; a shared member is the only guaranteed information path between two groups that may never talk directly
  3. 3
    Two subsets of a set of N nodes are guaranteed to intersect if and only if their sizes sum to more than N.
    forced by · if they summed to N or less, they could be disjoint, and a common element would not be guaranteed
  4. 4
    If all quorums are the same size Q, the requirement becomes 2Q > N, which gives Q > N/2 — a strict majority.
    forced by · this is the smallest size for which any two quorums must overlap, regardless of which nodes each contains
  5. 5
    Choosing anything smaller, such as two out of five, permits two disjoint quorums, each unaware of the other, each committing a different value.
    forced by · two disjoint groups of two exist within five nodes, so a partition can produce exactly that split
⇒ Therefore

Therefore the majority requirement is a set-theoretic necessity: it is the minimum condition under which two decisions must share a witness. It is not a conservative convention.

And note what this predicts: cluster sizes should be odd, because five nodes tolerate two failures and six nodes also tolerate only two — the sixth node adds cost and latency while adding nothing to fault tolerance. It also predicts that read and write quorums need only satisfy R + W > N rather than both being majorities, which is exactly the tunable knob Dynamo-style systems expose. And it predicts why witness or arbiter nodes work: they contribute to the intersection without storing data, because all the overlap argument requires is a participant, not a replica.

Mental modelA shared append-only log with one writer at a time

Consensus is not about agreeing on a value; it is about agreeing on the order of a log. One node is elected to propose entries, and an entry becomes committed once a majority has stored it. Every replica applies committed entries in order, so every replica reaches the same state.

Terms are logical time. Every message carries a term, and any node seeing a higher term immediately steps down — which is how a stale leader discovers it is stale without any central authority telling it.

  • Commit means a majority has durably stored the entry. Only committed entries may be applied and returned to clients — an entry present on the leader but not yet on a majority can still be lost, and treating it as done is the bug consensus exists to prevent.
  • A candidate can only win an election if its log is at least as up to date as a majority's. This single restriction is what guarantees a new leader already holds every committed entry, so nothing committed is ever lost.
  • Consensus gives you an ordered log, not a database. Everything else — state machines, leader leases, membership changes, snapshots — is built on top, which is why Raft appears as a component inside etcd, Kafka's controller, and every distributed database rather than as a product.
  • Latency is bounded by the round trip to the slowest member of the majority. This is why consensus groups are kept within a region or a small number of nearby regions, and why a geographically stretched cluster is slow on every single write rather than only during failures.
🔔 Fires when you see

Fire this model when you see: an etcd or ZooKeeper cluster with an even number of nodes · a cluster spanning distant regions · repeated leader elections under load · a system that lost data during a partition · someone implementing leader election with a database lock.

The tradeoff

You need to coordinate distributed state. Adopt a consensus system, or design so consensus is unnecessary?

Use a consensus system (etcd, ZooKeeper, or an embedded Raft)
+ you gain correct linearizable coordination, obtained from an implementation that has been tested far more thoroughly than anything you would write. Leader election, distributed locks, configuration and membership all become available immediately and correctly.
− you pay every coordinated operation costs a quorum round trip, throughput is bounded by what one leader can sequence, and you have added a critical stateful dependency that must be operated — backed up, upgraded, and understood. When it degrades, everything depending on it degrades.
pick when when you genuinely need a single agreed answer: cluster membership, leader election, configuration that must not diverge
Design consensus away
+ you gain no coordination means no coordination cost: unbounded scaling, no shared failure domain, and no additional system to run. Idempotent operations, CRDTs, partitioned ownership and event sourcing can eliminate the need entirely.
− you pay the design work is substantial and the constraints are real — you must express your logic in commutative or idempotent terms, which is not always possible. Reasoning about correctness becomes harder because there is no single order to point at.
pick when high-volume data paths where coordination would be the bottleneck, and the operations can be made commutative or partitioned by key
Consensus for control, coordination-free for data
+ you gain the control plane makes rare decisions — who owns which partition, what the current configuration is — using consensus, while the data plane operates locally within its assignment at full speed with no coordination per request.
− you pay two systems with two consistency models, and the boundary between them must be designed carefully: what happens to in-flight data-plane work when the control plane reassigns ownership is a genuine correctness question with no default answer.
pick when essentially every large distributed system, because it is the only shape that scales while remaining correct
What a senior engineer actually does

Never implement consensus yourself. The algorithms are subtle, the failure cases are rare and catastrophic, and correct implementations took years of production hardening plus formal verification to reach their current state. Use etcd, ZooKeeper, or a well-tested library.

More importantly, put consensus on the control path and keep it off the data path. The systems that scale are the ones where a coordinated decision is made rarely and cached — assign a partition once, then serve millions of requests against that assignment with no further coordination. Consensus per request is the design that looks correct on a whiteboard and cannot be made fast afterwards.


(c) Hands-on · 25 min

Let's build a toy Raft — enough to see leader election happen. We won't do full log replication (that's 500+ lines); we'll implement the election protocol so you can watch a partition heal and re-elect.

#!/usr/bin/env python3
"""raft_election.py — a toy of Raft's leader election in one file.
 
Run:  python raft_election.py
No deps. Simulates 5 nodes, a network with adjustable drop rate,
and prints a timeline of terms + votes + wins.
 
Not production Raft — no log replication, no persistence, no snapshots.
Enough to feel the protocol.
"""
from __future__ import annotations
 
import random
import threading
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
 
random.seed(7)
 
 
class Role(Enum):
    FOLLOWER = "F"
    CANDIDATE = "C"
    LEADER = "L"
 
 
@dataclass
class VoteReq:
    term: int
    candidate: str
 
 
@dataclass
class VoteResp:
    term: int
    granted: bool
    voter: str
 
 
@dataclass
class Heartbeat:
    term: int
    leader: str
 
 
class Network:
    """Simulated network with a drop rate + latency range."""
    def __init__(self, drop: float = 0.0, latency: tuple[float, float] = (0.005, 0.02)):
        self.drop = drop
        self.latency = latency
        self.nodes: dict[str, "Node"] = {}
        self.partitions: set[frozenset[str]] = set()
 
    def register(self, node: "Node") -> None:
        self.nodes[node.name] = node
 
    def partition(self, group_a: set[str], group_b: set[str]) -> None:
        """Prevent any message between group_a and group_b."""
        for a in group_a:
            for b in group_b:
                self.partitions.add(frozenset([a, b]))
 
    def heal(self) -> None:
        self.partitions.clear()
 
    def send(self, src: str, dst: str, msg: object) -> None:
        if frozenset([src, dst]) in self.partitions:
            return
        if random.random() < self.drop:
            return
        delay = random.uniform(*self.latency)
        def deliver() -> None:
            self.nodes[dst].receive(src, msg)
        threading.Timer(delay, deliver).start()
 
 
class Node:
    def __init__(self, name: str, peers: list[str], net: Network):
        self.name = name
        self.peers = peers
        self.net = net
        self.role = Role.FOLLOWER
        self.term = 0
        self.voted_for: Optional[str] = None
        self.leader: Optional[str] = None
        self.votes_received: set[str] = set()
        self.lock = threading.Lock()
        self.stopped = False
        self._reset_election_timer()
        # Start the tick loop
        threading.Thread(target=self._tick, daemon=True).start()
 
    def _reset_election_timer(self) -> None:
        self.election_deadline = time.time() + random.uniform(0.15, 0.30)
 
    def _tick(self) -> None:
        while not self.stopped:
            time.sleep(0.01)
            with self.lock:
                if self.role == Role.LEADER:
                    self._send_heartbeats()
                elif time.time() > self.election_deadline:
                    self._start_election()
 
    def _start_election(self) -> None:
        self.role = Role.CANDIDATE
        self.term += 1
        self.voted_for = self.name
        self.votes_received = {self.name}
        self._reset_election_timer()
        self._log(f"election started, term={self.term}")
        for p in self.peers:
            self.net.send(self.name, p, VoteReq(self.term, self.name))
 
    def _send_heartbeats(self) -> None:
        for p in self.peers:
            self.net.send(self.name, p, Heartbeat(self.term, self.name))
 
    def receive(self, src: str, msg: object) -> None:
        with self.lock:
            if isinstance(msg, VoteReq):
                self._handle_vote_req(msg)
            elif isinstance(msg, VoteResp):
                self._handle_vote_resp(msg)
            elif isinstance(msg, Heartbeat):
                self._handle_heartbeat(msg)
 
    def _handle_vote_req(self, m: VoteReq) -> None:
        if m.term > self.term:
            self.term = m.term
            self.voted_for = None
            self.role = Role.FOLLOWER
        grant = (m.term == self.term
                 and (self.voted_for is None or self.voted_for == m.candidate))
        if grant:
            self.voted_for = m.candidate
            self._reset_election_timer()
        self.net.send(self.name, m.candidate, VoteResp(self.term, grant, self.name))
 
    def _handle_vote_resp(self, m: VoteResp) -> None:
        if self.role != Role.CANDIDATE or m.term != self.term:
            return
        if m.granted:
            self.votes_received.add(m.voter)
            majority = (len(self.peers) + 1) // 2 + 1
            if len(self.votes_received) >= majority:
                self.role = Role.LEADER
                self.leader = self.name
                self._log(f"WON election, term={self.term}, votes={self.votes_received}")
 
    def _handle_heartbeat(self, m: Heartbeat) -> None:
        if m.term >= self.term:
            self.term = m.term
            self.role = Role.FOLLOWER
            self.leader = m.leader
            self.voted_for = None
            self._reset_election_timer()
 
    def _log(self, msg: str) -> None:
        print(f"[{time.time()%100:6.2f}] {self.name}: {msg}")
 
    def stop(self) -> None:
        self.stopped = True
 
 
def main() -> None:
    net = Network(drop=0.0)
    names = ["n1", "n2", "n3", "n4", "n5"]
    nodes = {n: Node(n, [p for p in names if p != n], net) for n in names}
    for n in nodes.values():
        net.register(n)
 
    print("--- start: watch a leader emerge ---")
    time.sleep(1.5)
 
    # Simulate a partition isolating the current leader
    leader = next((n for n in nodes.values() if n.role == Role.LEADER), None)
    if leader:
        others = {n.name for n in nodes.values() if n.name != leader.name}
        print(f"\n--- partition {leader.name} away from the rest ---")
        net.partition({leader.name}, others)
        time.sleep(1.5)
        print("\n--- heal the partition ---")
        net.heal()
        time.sleep(1.0)
 
    for n in nodes.values():
        n.stop()
 
 
if __name__ == "__main__":
    main()

What each block does

Anatomy of the script

Role enum + Node dataclass
Every Raft node lives in exactly one of three states. Term is monotonic — the number that lets nodes reject stale messages.
state
Network with partition()
Simulated unreliable transport. partition({A}, {B,C,D,E}) blocks any messages between the isolated node and the rest — mimicking a real network split.
chaos
_reset_election_timer
The randomised 150-300 ms timeout is Raft's answer to FLP: staggering candidates so votes rarely split.
timeout
_start_election
Increment term, vote for self, ask peers. Note: the term jump ALONE causes stale leaders (if they wake up) to step down when they see it.
election
_handle_vote_req · higher term rule
If a candidate's term > mine, I step down to follower and clear my vote. This is what lets partitioned-old-leaders bow out gracefully.
safety
majority check
(N // 2) + 1 for odd N. For 5 nodes: 3. This is the entire reason Raft works — quorums overlap.
quorum
Heartbeat handler
A heartbeat from a same-or-higher term leader resets your election timer AND drops you back to follower. This is how a healthy cluster stays quiet.
steady-state
Try itCause a split-brain — and watch Raft refuse to let it commit anything

Modify the partition call:

net.partition({"n1", "n2"}, {"n3", "n4", "n5"})

Watch the two-node group elect candidates that never win. Set net.heal() after 2 seconds and observe the higher-term leader from the group of 3 win everyone back. Now try net.partition({"n1","n2","n3"}, {"n4","n5"}) — the group-of-3 wins (has majority) and the group-of-2 fails.

💡 Hint · Partition 5 nodes into groups of 2 and 3. The group of 3 will elect a leader (has quorum). The group of 2 will endlessly hold elections (never reaches quorum of 3). Heal the partition and the group-of-2 nodes will discover a higher term via heartbeat and step down. No inconsistent state ever became durable — because durability requires a majority ack, which the minority never had.

(d) Production reality · 15 min

War story Cloudflare· 202027-minute global outage of Workers KV
🔥 What broke

A misconfigured deploy to Cloudflare's global etcd cluster (used as the control plane for Workers KV) caused a majority of etcd nodes to fail health checks and be removed from the cluster. Suddenly the surviving nodes were fewer than quorum.

Raft did exactly what it's supposed to do: it stopped accepting writes rather than risk inconsistency. But that meant the Workers KV control plane was frozen — no new key writes could propagate globally.

🧯 The fix
Manual intervention to bring the removed nodes back and re-establish quorum. The post-mortem added guardrails to health-check thresholds so a bad deploy could not evict more than N/2 nodes at once.
🎓 Lesson to steal
Raft's safety guarantees include "when in doubt, be unavailable". Losing quorum = writes stop, period. Operations teams must plan for the "healthy cluster, no quorum" failure mode — it's rare but always human-caused.
Post-mortem
War story Kubernetes at scale (multiple postmortems)hundreds of clusters over the years
🔥 What broke
etcd is the single source of truth for a Kubernetes cluster's entire state. When etcd goes slow (disk full, memory pressure, network partition), the API server times out, controllers stop reconciling, and a rolling deploy freezes mid-rollout. Symptoms look like "the cluster is broken" but the root cause is almost always etcd.
🧯 The fix
Standard playbook: (a) put etcd on dedicated SSD (etcd is fsync-heavy — a shared disk kills it), (b) monitor etcd's leader-changes-per-hour metric (a healthy cluster has 0), (c) never run etcd across regions (100 ms cross-region latency = 100 ms per write, exhausts the write budget in minutes).
🎓 Lesson to steal
The consensus layer is the beating heart of everything above it. Treat it like a database — SLOs, dedicated disks, capacity planning — not like "just another Kubernetes component". Every etcd operator has a story about "the disk got 90% full and nothing else worked".
Post-mortem
War story Common failure mode across all Raft systemsdocumented in etcd, Consul, CockroachDB user forums
🔥 What broke
Team runs a 3-node Raft cluster across 3 availability zones for HA. One AZ has a brief network hiccup — its node is unreachable for 30 s. During those 30 s, one of the two surviving nodes crashes. Now 2 of 3 nodes are unreachable. Cluster loses quorum, writes freeze, alerts fire.
🧯 The fix
Move to a 5-node cluster spread across 3 AZs (2+2+1). Now the loss of any two nodes still leaves 3 alive = quorum. Doubles the cost, but doubles the availability. For write-heavy workloads add "learner" nodes (Raft's non-voting replicas) to spread reads without slowing writes.
🎓 Lesson to steal
"3 nodes for HA" tolerates only 1 failure. If your product requires "can lose 2 things at once", you need 5. The math is fixed by the algorithm — you cannot argue with quorum.

Where this shows up in the rest of the plan

Consensus underpins the whole distributed-systems tower
S072 · Consistency models
The goal (linearizability) that this session's algorithm delivers.
S074 · Sharding
Each shard is typically its own Raft group. 1000 shards = 1000 Raft groups running independently.
S076 · Multi-region
Raft across continents = 100+ ms writes. This is why 'global consistency' is expensive — you're paying for global consensus.
S090 · Kubernetes internals
etcd IS a Raft cluster. Understanding this session means understanding half of what kube does.
S105 · Kafka internals
Kafka's KRaft controller uses Raft to replace the old ZooKeeper dependency. Same algorithm, different codebase.
S127 · Design a lock service (interview)
The classic 'build Chubby / etcd' interview question. Answer: 3-5 nodes, Raft, TTL on locks, lease renewals.

(e) Recall + stretch · 10 min

Quick recall · click to reveal
★ = stretch question

Explain-out-loud test

Teach these three, no notes:

  1. Why is consensus hard? (FLP + the group-chat analogy)
  2. Walk through a Raft leader election in 60 seconds.
  3. Why do 3-node Raft clusters tolerate only 1 failure? (majority math)

Hub: The 6-Month Learning Plan


Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.