Home › Distributed systems › Consensus
algorithm medium

Raft

Problem Several machines must agree on an ordered log of commands and keep identical state, surviving failures, for metadata, locks, configuration or sharded databases. This is the understandable consensus protocol for that.

Leader-based consensus on a replicated log; one leader per term, majority commit; the default for a strongly consistent replicated state machine.

commit 1 round trip leader→majority + 1 fsync; 2f+1 nodes tolerate f failures
consensusreplicated-logleader-electionstate-machine-replication
Overview

Replicas agree on an ordered log of commands; applying it in order yields identical state machines. One leader per term (randomized election timeouts, one vote per term) appends entries, followers accept only if their log matches at the previous index, and only nodes with up-to-date logs can win. Paxos-equivalent safety, small enough to hold in your head.

Key facts

Use when

  • Linearizable metadata, config, locks or a replicated log (etcd, Consul).
  • Sharded data with one group per range (CockroachDB, TiKV).
  • Message-broker metadata and queues (Kafka KRaft, RabbitMQ quorum queues), cluster orchestration state (Kubernetes via etcd).

Avoid when

Deep dive

How it works

Parts:

  • persistent state — term, voted_for, log[], fsynced before answering any RPC
  • election timer — randomized timeout that starts an election
  • replication tracker (leader) — next_index[], match_index[] per follower
  • commit index — highest entry known to be on a majority
  • state machine — applies committed entries in log order

Operations: election, replication, commit, new-leader no-op (steps below).

State per node: persistent term, voted_for, log[] (fsynced before answering any RPC); volatile commit_index; leader-only next_index[], match_index[].

  1. Election. A follower that hears nothing for a random timeout (e.g. 150-300 ms in the paper) increments term, votes for itself, sends RequestVote(term, last_index, last_term). A node grants at most one vote per term, and only if the candidate's log is at least as up to date (higher last term, or same last term and ≥ last index). Majority → leader.
  2. Replication. Leader appends the command locally and sends AppendEntries(term, prev_index, prev_term, entries, leader_commit). The follower rejects unless its entry at prev_index has prev_term (log matching property), truncates any conflicting suffix, appends.
  3. Commit. An entry is committed once stored on a majority and it is from the leader's current term; earlier-term entries commit indirectly with it. Followers learn commit_index from the next AppendEntries.
  4. New leader appends a no-op in its term so prior entries get committed quickly.
roles: Follower ─timeout─► Candidate ─majority─► Leader ─higher term seen─► Follower

client ──► Leader ──AppendEntries──► F1 (append, fsync, ack)
             │  └────────────────────► F2 (append, fsync, ack)
             └ commit when 2 of 3 hold entry from current term ─► apply ─► reply

Code

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Role { Follower, Candidate, Leader }

#[derive(Clone, Debug)]
pub struct Entry { pub term: u64, pub cmd: Vec<u8> }

pub struct AppendReq {
    pub term: u64, pub prev_index: usize, pub prev_term: u64,
    pub entries: Vec<Entry>, pub leader_commit: usize,
}
// On failure `index` is a hint: the leader retries from index + 1.
pub struct AppendResp { pub term: u64, pub success: bool, pub index: usize }

pub struct Raft {
    pub id: usize,
    pub n: usize,                  // cluster size including self
    pub term: u64,                 // persistent
    pub voted_for: Option<usize>,  // persistent
    pub log: Vec<Entry>,           // persistent; log[0] = sentinel {term: 0}
    pub role: Role,
    pub commit: usize,
    pub next_index: Vec<usize>,
    pub match_index: Vec<usize>,
}

impl Raft {
    fn last(&self) -> (usize, u64) { let i = self.log.len() - 1; (i, self.log[i].term) }

    fn observe_term(&mut self, term: u64) {
        if term > self.term { self.term = term; self.voted_for = None; self.role = Role::Follower; }
    }

    pub fn on_request_vote(&mut self, term: u64, cand: usize, last_idx: usize, last_term: u64) -> (u64, bool) {
        self.observe_term(term);
        let (li, lt) = self.last();
        let up_to_date = last_term > lt || (last_term == lt && last_idx >= li);
        let grant = term == self.term && up_to_date && self.voted_for.map_or(true, |v| v == cand);
        if grant { self.voted_for = Some(cand); } // persist + reset election timer before replying
        (self.term, grant)
    }

    pub fn on_append(&mut self, r: AppendReq) -> AppendResp {
        if r.term < self.term { return AppendResp { term: self.term, success: false, index: 0 }; }
        self.observe_term(r.term);
        self.role = Role::Follower;                    // a candidate in the same term yields
        // reset election timer here
        if r.prev_index >= self.log.len() {
            return AppendResp { term: self.term, success: false, index: self.log.len() - 1 };
        }
        if self.log[r.prev_index].term != r.prev_term {
            return AppendResp { term: self.term, success: false, index: r.prev_index - 1 };
        }
        let n = r.entries.len();
        for (k, e) in r.entries.into_iter().enumerate() {
            let i = r.prev_index + 1 + k;
            if i < self.log.len() {
                if self.log[i].term == e.term { continue; } // already have it: never truncate on a stale RPC
                self.log.truncate(i);
            }
            self.log.push(e);
        }
        let last_new = r.prev_index + n;
        if r.leader_commit > self.commit { self.commit = r.leader_commit.min(last_new); }
        AppendResp { term: self.term, success: true, index: last_new } // fsync log before replying
    }

    pub fn become_leader(&mut self) {
        self.role = Role::Leader;
        self.log.push(Entry { term: self.term, cmd: Vec::new() }); // no-op commits older entries
        self.next_index = vec![self.log.len(); self.n];
        self.match_index = vec![0; self.n];
    }

    pub fn on_append_resp(&mut self, from: usize, resp: AppendResp) {
        self.observe_term(resp.term);
        if self.role != Role::Leader || resp.term != self.term { return; }
        if resp.success {
            self.match_index[from] = self.match_index[from].max(resp.index);
            self.next_index[from] = self.match_index[from] + 1;
            self.advance_commit();
        } else {
            let hinted = (resp.index + 1).min(self.next_index[from]);
            self.next_index[from] = hinted.max(self.match_index[from] + 1);
        }
    }

    fn advance_commit(&mut self) {
        let mut m = self.match_index.clone();
        m[self.id] = self.log.len() - 1;
        m.sort_unstable();
        let majority_idx = m[(self.n - 1) / 2]; // held by at least a majority
        // Only count replicas for entries of the current term (Raft paper, Fig. 8).
        if majority_idx > self.commit && self.log[majority_idx].term == self.term {
            self.commit = majority_idx;
        }
    }
}

Complexity and constants

  • Messages per commit: 2(n−1) (AppendEntries + ack); with batching, one RPC carries many entries.
  • Latency: 1 RTT to the fastest majority + max(leader fsync, follower fsync) if done in parallel. Datacenter RTT order of 0.1-0.5 ms; fsync order of 20 µs (NVMe with power-loss protection) to several ms (consumer SSD, cloud volumes).
  • Throughput: leader CPU + NIC bound; batched + pipelined implementations reach order of 10⁴-10⁵ small writes/s per group.
  • Failover: election timeout + one round of voting, i.e. order of 1-2× the election timeout.

Efficiency

  • Cost: 2(n−1) messages per commit (AppendEntries plus ack), fewer per entry when batched. Latency is 1 RTT to the fastest majority plus the larger of leader and follower fsync when done in parallel: datacenter RTT order of 0.1-0.5 ms, fsync order of 20 µs (NVMe with power-loss protection) to several ms (consumer SSD, cloud volumes). Batched, pipelined implementations reach order of 10⁴-10⁵ small writes/s per group. Bound: fsync latency, then leader CPU and NIC.
  • Levers:
    • batch many entries per AppendEntries and per fsync — amortizes RTT and fsync (group commit; Ongaro thesis) — any write load
    • pipeline AppendEntries without waiting for acks — hides RTT (Ongaro thesis) — high-latency links
    • leader writes to disk in parallel with replication — commit waits for max, not sum, of fsync and RTT (Ongaro thesis) — all deployments
    • one Raft group per range or region (multi-Raft, CockroachDB, TiKV) — one leader no longer bounds throughput — large datasets
    • read-only queries without a log entry (ReadIndex or leader lease, Ongaro thesis) — no fsync on reads — read-heavy loads
  • Naive → standard → state of the art:
    • one RPC and one fsync per entry · RTT plus fsync, about 0.2-2 ms per write
    • batched entries, parallel leader fsync · many writes per RTT and fsync
    • batching, pipelining and multi-Raft sharding · order of 10⁴-10⁵ writes/s per group, scaled out by group count

Implementation notes

  • Persist before replying: term, voted_for, and log entries must be durable before any RPC response, or a restarted node can double-vote. Use a write-ahead log and group commit.
  • Pipeline: the leader keeps sending AppendEntries without waiting for acks (optimistically bumps next_index); fsync locally in parallel with network send.
  • Pre-vote (thesis §9.6): a partitioned node first asks whether it would win before bumping its term, so it can't disrupt a healthy leader on rejoin. CheckQuorum: a leader steps down if it hasn't heard from a majority within an election timeout.
  • Linearizable reads without writing to the log: ReadIndex (leader records commit index, confirms leadership with a heartbeat round, waits until applied) or lease reads (skip the heartbeat while a lease is valid; needs bounded clock drift).
  • Snapshots: compact the log; send InstallSnapshot to followers that fell behind the compaction point.
  • Membership change: single-server changes (add/remove one at a time) or joint consensus; never switch configs for several nodes at once.
  • Multi-Raft: thousands of groups per process; coalesce heartbeats per node pair and quiesce idle groups.

Pitfalls

  • Committing a previous-term entry by counting replicas (the Figure 8 bug): it can be overwritten. Only count for current-term entries.
  • Truncating the log on every AppendEntries instead of only on a term conflict: a delayed RPC then deletes committed entries.
  • Election timeout too close to heartbeat interval or RTT/fsync tail: endless elections. Keep timeout ≥ 10× heartbeat and well above p99 fsync.
  • Serving reads from the leader without ReadIndex/lease: a deposed leader returns stale data.
  • Even cluster sizes: 4 nodes tolerate 1 failure, same as 3, with a larger quorum.

Variants and alternatives

In production

Configuration and orchestration: etcd (etcd-io/raft, used by Kubernetes), HashiCorp Consul/Nomad/Vault (hashicorp/raft). Databases: CockroachDB and TiKV (raft-rs, multi-Raft per range/region), YugabyteDB. Messaging: Kafka KRaft metadata quorum, RabbitMQ quorum queues (Ra).

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
openraftRustdatabendlabs/openraftcargo add openraft————async Raft, pluggable storage and network
raft-rsRusttikv/raft-rscargo add raft————consensus core only (TiKV); you supply storage and transport
etcd-io/raftGoetcd-io/raftgo get go.etcd.io/raft/v3————core library behind etcd
hashicorp/raftGohashicorp/raftgo get github.com/hashicorp/raft————used by Consul, Nomad, Vault
RaErlangrabbitmq/ran/a (Erlang/Elixir library, ra on Hex)————RabbitMQ quorum queues

Sources

  • Diego Ongaro, John Ousterhout, In Search of an Understandable Consensus Algorithm, USENIX ATC 2014. paper · talk page
  • Diego Ongaro, Consensus: Bridging Theory and Practice, PhD thesis, Stanford, 2014. thesis
  • Heidi Howard, Richard Mortier, Paxos vs Raft: Have We Reached Consensus on Distributed Consensus?, PaPoC 2020. paper

Builds on

Used by

linearizable reads · plannedReadIndex and leader-lease reads without writing to the log
multi raft · plannedThousands of Raft groups per node: batched heartbeats, shared log engine, range split/merge

Systems (33):

Alternatives and related

Multi-PaxosReplicated systems need several machines to agree on one ordered log of commands despite failures. This is the classic consensus protocol, preferred when you need more flexibility than Raft offers.
viewstamped replication · plannedVSR: primary-backup consensus with view changes and recovery
bft consensus · plannedByzantine fault tolerant consensus (PBFT, Tendermint, HotStuff) with 3f+1 replicas and quorum certificates
Chain replicationA storage system keeping several copies of data in one datacenter must stay consistent and survive node crashes while moving large amounts of data. This is a way to replicate writes without voting.Primary-backup replicationA stateful service on one machine must keep running after that machine dies, without losing updates it already confirmed to clients, at the price of extra machines and some added write latency.Quorum replicationA data store spread across replicas must stay available and fast even when some replicas are slow or down, accepting weaker guarantees in exchange. It suits workloads like sessions, carts and time series.Group commitA database log, queue or consensus system with many concurrent writers that each need data durably on disk before replying is limited by the cost of each disk flush.Two-phase commitA transaction touching several shards or databases must either take effect everywhere or nowhere, even if a participant or the coordinator crashes mid-way. This gives that all-or-nothing outcome.fsync, O_DSYNC and O_DIRECTHow do you make sure data written by a program actually survives a crash or power loss? This guide answers which file-write calls really guarantee durability on Linux and macOS, and in what sequence to use them.