Consensus and Raft: One Truth at the Price of a Quorum
By Sergey Nosov
16 October 2026
Suppose a sign-up service keeps its accounts on two replicas, and a network partition cuts them apart. On each side, someone asks for the username kestrel, and each replica says yes. When the network heals, the replicas can merge their data, but no merge can repair the result. Two people were promised the same name, and one of them will lose it.
Many operations never face that problem. Counting likes, tracking who is online, and editing a shared document can all run on every replica and merge later. Others have no safe merge: claiming a unique username, spending from a balance that must not go negative, choosing the one node that leads, changing the cluster’s own membership. There the replicas must agree before anyone gets an answer. That is consensus: getting a cluster of unreliable machines to act as one reliable machine.
Consensus buys one truth at the price of a quorum. This article explains when that price is worth paying and how Raft, the algorithm inside etcd, Consul, and RabbitMQ’s quorum queues, pays it. It shows Raft’s rules in C#, tested on .NET 10, and what a three-node etcd 3.7.2 cluster does when it loses its leader and then its majority.
Merge or Agree
A CRDT, short for conflict-free replicated data type, lets every replica accept writes on its own. In the state-based form, its merge is commutative, associative, and idempotent. Replicas that have seen the same updates therefore reach the same state, whatever the order of arrival and however often an update was repeated. A grow-only counter, for example, keeps one count per replica, and a merge takes the larger value of each.
Whether merging is enough has a precise answer. In a 2014 paper, “Coordination Avoidance in Database Systems,” Peter Bailis and five coauthors named the deciding property invariant confluence. Informally, it holds when “divergent but I-valid database states can be merged into a valid database state.” An I-valid state satisfies the application’s invariants, the rules its data must always obey.
The paper proves the property necessary and sufficient. Where it holds for a set of operations and invariants, replicas can run them without coordination, stay available, and converge without breaking an invariant. Where it fails, no system can promise all of that. Invariant confluence draws the line where practitioners need it:
- Uniqueness. Two replicas can each give the same ID to a different user. Each state is valid on its own, but their merge is not, and the paper concludes that “uniqueness is not I-confluent for inserts of unique values.” It distinguishes a request for a specific unique ID from a request for any unique ID. The second needs no coordination on each insert. If every replica has a known ID and its own portion of the ID space, its IDs stay unique, as long as they need not be sequential. A username is the first kind.
- Thresholds. The paper names the classic case in its introduction: withdrawals that each pass on their own can together leave a balance negative. For counters, its analysis shows that a rule keeping the value above a threshold survives concurrent increments but not decrements. The escrow method amortizes the coordination that withdrawals need. In the paper’s extended version, a $100 balance split among five servers lets each dispense $20 without coordinating, until it must refresh its share.
- Leaders, locks, and membership. Two leaders, two holders of one lock, or two versions of the cluster’s own membership cannot be merged into one.
So ask of each operation: can two replicas each say yes on their own and still be right after they merge? If they can, merge, and keep answering during a partition, as my article on the CAP theorem describes. If they cannot, the replicas must agree first, because a merge that keeps both answers is precisely the bug.
Why Agreement Is Hard
First, you cannot tell slow from dead. A node that goes silent may have crashed, or its messages may be stuck in a congested network. Every failure detector guesses, usually with a timeout.
Second, a wrong guess invites split brain. If each half of a partitioned cluster decides that the other half is dead and acts on that guess alone, both halves may elect a leader and accept writes. Two truths then diverge, and two truths are worse than none.
Third, a proof shows what the first fact costs. In 1985, Michael Fischer, Nancy Lynch, and Michael Paterson published what they called “the surprising result that no completely asynchronous consensus protocol can tolerate even a single unannounced process death.” Their model bounds neither process speeds nor message delays and has no synchronized clocks, “so algorithms based on time-outs, for example, cannot be used.” No process can tell whether another “has died (stopped entirely) or is just running very slowly.” One crash at the wrong moment can then keep any deterministic protocol in that model from ever deciding.
The result, known as FLP after its authors, does not make consensus impossible in practice, and the authors said so. They called for models with more realistic assumptions about timing. Practical algorithms rely on such assumptions, but only for progress. The Raft paper requires that “safety must not depend on timing.” Whatever the delays, losses, duplication, or reordering of messages, Raft never returns an incorrect result, provided that nodes fail only by stopping and keep what they stored. Timing decides only when the cluster moves forward: it needs messages to arrive well within its election timeout.
Raft does not tolerate nodes that lie. Such faults are called Byzantine, after a 1982 paper by Leslie Lamport, Robert Shostak, and Marshall Pease, and tolerating them costs more. Miguel Castro and Barbara Liskov wrote in 1999 that where messages may be delayed, lost, or reordered, “3f+1 is the minimum number of replicas” that can survive f faulty ones. Raft survives f crashes with 2f + 1 nodes. When nodes merely crash instead of lie, agreement gets a full node cheaper per failure.
Raft in Three Ideas
Before Raft, the standard answer was Leslie Lamport’s Paxos, which Diego Ongaro and John Ousterhout of Stanford University called “exceptionally difficult to understand.” Their 2014 paper, “In Search of an Understandable Consensus Algorithm,” introduced Raft with understandability as its primary goal. It was important, they wrote, “not just for the algorithm to work, but for it to be obvious why it works.”
Raft keeps a replicated log. Every node runs the same deterministic state machine, such as a key-value store, and applies the commands in its log in order. If every log holds the same commands in the same order, every node reaches the same state. Raft’s job is to keep the logs consistent, and it does that with three ideas.
Idea 1: Elect One Leader
Every node is a follower, a candidate, or the leader. Time is divided into numbered terms, which act as a logical clock. Every message carries its sender’s term. A node that sees a higher term adopts it, and a node that receives a message from an older term rejects it. Each term begins with an election.
The leader sends heartbeats. A follower that hears nothing for its election timeout assumes the leader is gone: it increments its term, votes for itself, and asks the others for their votes. Each node grants at most one vote per term, first come, first served, and a candidate that collects votes from a majority leads for that term. Each node records its vote on disk before it answers. Any two majorities of the same membership share a node, so two candidates cannot both win the same term.
Each node draws its election timeout at random from a range; the paper’s example is 150 to 300 milliseconds. Usually one node times out first and wins before the others time out. When two become candidates at once and split the vote, the term ends without a leader, and fresh random timeouts break the tie.
One leader per term makes the leader the cluster’s single point of order without making it a single point of failure: losing it only triggers the next election. The rule promises at most one leader per term, not at most one node that believes it is the leader. A leader that has been cut off can keep believing for a while, and the experiment below caught exactly that.
Idea 2: Replicate a Log
Clients send commands to the leader. The leader appends each command to its own log and sends it to the followers in an AppendEntries message. A heartbeat is simply an AppendEntries message with no entries, so the data plane and the failure detector are one mechanism.
Each AppendEntries also names the index and term of the entry just before the new ones. A follower accepts the batch only if its own log has an entry at that index with that term. Otherwise it refuses, and the leader retries from an earlier entry until the two logs match.
From there, the follower deletes any entry that conflicts with a new one, along with all that follow it, and appends the new entries it lacks. As a result, two logs that hold an entry with the same index and term are identical up to that entry: the Log Matching Property.
The paper calls an entry committed, and safe to apply, once “the leader that created the entry has replicated it on a majority of the servers.” The overlap that keeps two leaders out of one term also protects that entry: every later majority contains a node that holds it. The truth cannot fork.
Idea 3: Three Safety Rules
- The election restriction. A node refuses its vote to a candidate whose log is less up to date than its own. Raft compares the last entries: the later term wins, and if the terms are equal, the longer log wins. A committed entry sits on a majority, and a winner needs votes from a majority, so every new leader already holds every committed entry.
- Leaders only append. A leader never overwrites or deletes entries in its own log. It repairs a lagging follower by finding the last entry they share and replacing the follower’s conflicting entries after it.
- Old entries commit only behind new ones. A new leader may hold entries from earlier terms that already sit on a majority. The paper’s Figure 8 shows that such an entry can still be overwritten by a later leader, so “Raft never commits log entries from previous terms by counting replicas.” They become committed when an entry from the leader’s current term commits after them. That is also how a new leader learns what is committed: Raft has each leader commit a no-op entry when its term begins.
All three ideas lean on the majority. It elects the leader, commits each entry, and carries every committed entry into the next term. The majority is the unit of truth. Lose it, and a well-built system refuses to guess.
The Rules in C#
The sample below is the receiving side of Raft’s two messages. It follows Figure 2 of the paper, which specifies the messages, the state, and the rules on one page. It leaves out timers, roles, the leader’s side, snapshots, and membership changes. It also keeps its state in memory, while Figure 2 requires a node to save the term, the vote, and the log to stable storage before it responds.
public record LogEntry(
long Term, string Command);
public record AppendEntries(
long Term,
string LeaderId,
long PrevLogIndex,
long PrevLogTerm,
LogEntry[] Entries,
long LeaderCommit);
public record RequestVote(
long Term,
string CandidateId,
long LastLogIndex,
long LastLogTerm);
public record Reply(
long Term, bool Success);
Term is the leader’s term, and LeaderId lets a follower redirect clients. PrevLogIndex and PrevLogTerm say where the batch attaches. An empty Entries array is a heartbeat, and LeaderCommit reports how far the leader has committed. RequestVote carries the candidate’s last index and term for the election restriction. One Reply serves both messages: it returns the receiver’s term, so a stale sender learns it is stale, and whether the request succeeded.
public sealed class Node
{
// A real node persists these
// three before it replies.
long currentTerm;
string? votedFor;
readonly List<LogEntry> log = [];
long commitIndex;
public long CommitIndex => commitIndex;
public IReadOnlyList<LogEntry> Log => log;
long LastIndex => log.Count;
long LastTerm =>
log.Count == 0 ? 0 : log[^1].Term;
// Raft numbers entries from 1.
long TermAt(long i) =>
i == 0 ? 0 : log[(int)i - 1].Term;
// Drop entry i and the rest.
void TruncateFrom(long i) =>
log.RemoveRange((int)i - 1, log.Count - (int)i + 1);
Reply Refuse() => new(currentTerm, false);
Reply Accept() => new(currentTerm, true);
void ObserveTerm(long term)
{
if (term > currentTerm)
{
currentTerm = term;
votedFor = null;
}
}
public Reply Handle(RequestVote req)
{
ObserveTerm(req.Term);
if (req.Term < currentTerm)
return Refuse();
// Election restriction
var upToDate = req.LastLogTerm > LastTerm
|| (req.LastLogTerm == LastTerm
&& req.LastLogIndex >= LastIndex);
// One vote per term
var free = votedFor is null
|| votedFor == req.CandidateId;
if (!upToDate || !free)
return Refuse();
votedFor = req.CandidateId;
return Accept();
}
public Reply Handle(AppendEntries req)
{
ObserveTerm(req.Term);
// Refuse a stale leader
if (req.Term < currentTerm)
return Refuse();
// The batch must attach to
// a prefix this log holds
if (req.PrevLogIndex > LastIndex
|| TermAt(req.PrevLogIndex) != req.PrevLogTerm)
return Refuse();
// Cut only on a conflict
var index = req.PrevLogIndex;
foreach (var entry in req.Entries)
{
index++;
if (index <= LastIndex)
{
if (TermAt(index) == entry.Term)
continue;
TruncateFrom(index);
}
log.Add(entry);
}
// Commit what this request
// proved, never backward
commitIndex = Math.Max(commitIndex,
Math.Min(req.LeaderCommit, index));
return Accept();
}
}
Most lines map one to one onto Figure 2’s rules. Two hide traps, and the test harness behind the sample includes a shortcut version of each to show it failing:
- Cut only on a conflict. The obvious implementation truncates everything after
PrevLogIndexand appends the batch. A delayed message breaks it. In the test, a follower holds four entries and has acknowledged all of them when an old copy of an earlier AppendEntries, carrying only entry 3, arrives. The shortcut drops entry 4, which the leader may already have counted toward a majority;Nodekeeps it. Jon Gjengset’s “Students’ Guide to Raft” is blunt: if the follower already has all the entries the leader sent, it “MUST NOT truncate its log.” - Commit only what the request proved. Figure 2 sets the commit index to the smaller of
LeaderCommitand the “index of last new entry.” The Students’ Guide stresses using that index, not the end of the log. In the test, a follower still holds a deposed leader’s entry at index 3. The new leader’s message proves only entries 1 and 2 but reports a commit index of 3. Committing to the end of the log would apply the deposed leader’s entry, which the new leader never had. The sample commits through index 2.
Read literally, Figure 2’s commit rule can also move the commit index backward when a re-sent message proves less than the follower has already committed. The Math.Max call prevents that, the same guard that etcd’s raft library writes as “never decrease commit.” The harness runs twenty-four checks, covering both traps, that guard, stale leaders, one vote per term, and the election restriction.
Treat the sample as a way to read the rules, not as something to ship. Even etcd’s raft library implements only the algorithm: “both network and disk IO are left to the user.”
Watching Raft in etcd
Kubernetes documents etcd as its “backing store for all cluster data,” and etcd is a Raft cluster. The test cluster runs three etcd 3.7.2 nodes in containers on one virtual machine, with etcd’s default timing: a heartbeat every 100 milliseconds and an election timeout of 1,000 milliseconds. Each node draws its actual timeout at random between 1.0 and 1.9 seconds whenever it becomes a follower or a candidate.
Two Raft extensions are on. The server code enables CheckQuorum, under which a leader steps down when it cannot hear from a majority, and the --pre-vote flag defaults to true. The test cuts the leader off by disconnecting it from the container network. The leader keeps running, and a client on the same node can still reach it, but it cannot reach the other two nodes.
Across eight cuts, the node logs show a new leader at the next term about 0.9 to 1.5 seconds after the cut. A follower counts its timeout in 100-millisecond ticks from the last heartbeat, which arrived just before the cut. The election itself, from the first pre-vote request to the new leader, took 5 to 25 milliseconds.
Nearly all of the wait was the timeout, the price of not mistaking slow for dead. The Raft paper predicts it: after a leader crash, “the system will be unavailable for roughly the election timeout.” The logs show when the leader changed, not when clients could write again.
CheckQuorum demoted the old leader about 1.0 to 1.8 seconds after the cut, when it logged “stepped down to follower since quorum is not active.” In seven of the eight cuts, that came after the takeover, so for up to 0.8 seconds two nodes each believed they led, in different terms. The old one could commit nothing without a majority. Ongaro’s dissertation explains why the step-down exists: a cut-off leader that a client can still reach “could delay a request from that client forever.”
On the cut-off node, a write waited for etcdctl’s 5-second default timeout and failed, and so did an ordinary read. A serializable read answered at once. The transcript omits the warning line that the client printed before each error:
$ etcdctl put /demo/color red
Error: context deadline exceeded
$ etcdctl get /demo/color
Error: context deadline exceeded
$ etcdctl get /demo/color \
--consistency=s
/demo/color
blue
Reads in etcd are linearizable by default, and its documentation explains the cost: “linearized requests must go through the Raft consensus process.” A serializable read skips that step, and the same page warns that it “may access stale data with respect to quorum.” Here it returned blue, the value from before the cut, although the majority side had already changed it to green.
When the node was reconnected, it rejoined as a follower of the new leader, caught up, and returned green to an ordinary read. Its term had not moved while it was cut off, although it had started a pre-vote at that term every 1.5 seconds.
That is Pre-Vote, as the dissertation describes it. A candidate increments its term only “if it first learns from a majority of the cluster that they would be willing to grant the candidate their votes.” Without Pre-Vote, a returning node would bring back a larger term, which “will force the cluster leader to step down.” Here the other two nodes rejected the returning node’s pre-vote requests, and the leader kept its term.
Last, the test killed both followers, leaving the leader alone. In two runs, it went on believing it was the leader for about 1.5 and 2.0 seconds after the second follower died, until CheckQuorum demoted it. Writes and ordinary reads timed out again, and a health check said why (wrapped):
$ etcdctl endpoint health
127.0.0.1:2379 is unhealthy:
failed to commit proposal:
context deadline exceeded
Error: unhealthy cluster
A serializable read still answered with the last value the node had. When the two nodes restarted, the cluster elected a leader for the next term within 1.5 seconds of the restart command, and writes succeeded again. The etcd FAQ describes both kinds of recovery. After a partition, etcd resumes automatically and safely once the network recovers. After a power loss, it replays its log from disk and rejoins the cluster.
The Kubernetes documentation names the consequence: while etcd has no leader, “a cluster cannot make any changes to its current state, which implies no new pods can be scheduled.” If a majority of members fail for good, it adds, “the scheduled pods might continue to run.” Workloads may keep running, but the control plane cannot change them.
Where Raft Runs
You consume Raft far more often than you implement it, and you probably run it already:
- Kubernetes keeps its cluster state in etcd, at least in a standard control plane. K3s documents SQLite as its default datastore on a single server and offers embedded etcd or an external database for clusters with several servers. In November 2024, Google said it was moving GKE, its managed Kubernetes service, from etcd to a key-value store based on Spanner.
- HashiCorp Consul and Nomad run Raft among their servers, and so does Vault’s integrated storage. Consul recommends three or five servers for production.
- RabbitMQ describes its quorum queues as durable, replicated queues based on Raft. RabbitMQ 4.0, released in September 2024, removed classic queue mirroring, so quorum queues and streams are its replicated types.
- Apache Kafka keeps its cluster metadata in KRaft, which the proposal that designed it calls “a sort of Raft dialect.” The project’s announcement called Kafka 4.0, released on 18 March 2025, the first major release to run entirely without ZooKeeper. The same release added a Pre-Vote mechanism to KRaft.
- CockroachDB splits its data into ranges, each replicated by its own Raft group. In 2015, the Cockroach Labs blog said that “each node may be participating in hundreds of thousands of consensus groups.”
- MongoDB replica sets use what a 2021 paper by authors from MongoDB and Stony Brook University calls “a novel consensus protocol that derives from Raft.” Unlike Raft, where the leader pushes new data, a replica pulls it from any other replica.
Not every consensus system runs Raft. ZooKeeper uses its own protocol, called Zab. Google’s Spanner and Amazon’s DynamoDB use Paxos. What follows applies to every operation that goes through consensus, whichever protocol runs it.
What Consensus Costs
- Latency. A write commits only after a majority of the nodes have stored it, which in etcd means writing it to disk. A slow disk can even cost the leader its role: the etcd FAQ says that “disk latency is part of leader liveness.”
- Throughput. One leader orders every write in its group, so systems that need more split their data across many groups, as CockroachDB does.
- Availability. Nodes cut off from a majority can commit no writes, and at most they serve reads that may be stale. That is the honest behavior: a system that has lost its majority should go read-only or unavailable, never into split brain. Losing a majority for good is worse. The etcd documentation then calls for disaster recovery, and RabbitMQ says such a quorum queue “is permanently unavailable and will need to be force deleted and recreated.”
- Size. Three voting members tolerate one failure, five tolerate two, and four still tolerate only one. The etcd FAQ explains why the fourth member makes things worse: “exactly the same number of nodes may fail without losing quorum but there are more nodes that can fail.” An even cluster can also split evenly, leaving no majority on either side. And every voting member added slows writes, because each write must reach more machines.
- Membership. Each change needs a quorum too. To replace a failed voting member, the etcd FAQ says to remove it first, because adding a voter first raises the quorum while the failed member still counts. Then add the replacement as a non-voting learner, as the etcd documentation recommends, and promote it once it has caught up.
- Reads. A read that must reflect the latest write costs coordination too: the Raft paper says the leader “must check whether it has been deposed” before it answers.
What Consensus Leaves to You
- Acknowledgments. A product may acknowledge a write before consensus commits it. MongoDB does so when a client’s write concern asks for fewer acknowledgments than a majority. Its manual warns that two nodes may briefly both believe they are primary, as in the etcd test, and only the current one can complete majority-acknowledged writes. New writes to the former one, the manual adds, “will eventually roll back,” acknowledged or not.
- Retries. Consensus does not make a client’s retry safe. If a leader commits a command and crashes before it replies, the client retries with the next leader, and the command runs twice. The Raft paper’s fix is the one my article on idempotency describes: clients “assign unique serial numbers to every command.” The state machine then answers a repeated number without running the command again.
- Your own leaders. Many services elect their own leader through etcd, ZooKeeper, or a Kubernetes Lease. The store agrees on one holder, but a paused or cut-off former holder may not know it lost, just as the old etcd leader did not. Kubernetes’s client-go library warns that its leader election “does not guarantee that only one client is acting as a leader (a.k.a. fencing).” The remedy Martin Kleppmann describes is a fencing token, a number that grows with every lock grant. The protected resource rejects any token lower than the highest it has seen.
A Checklist
The Raft, leader election, and quorum entries in my Software Development Principles series list warning signs, such as a missing pre-vote step that lets a rejoining node disrupt the leader. Before you depend on consensus, ask:
- Can it merge? If two replicas can each say yes and still be right after merging, use a mergeable design and stay available.
- Is the implementation proven? Use a consensus system or library with a record of testing under partitions. Do not ship your own Raft.
- Is the voting membership odd? Run three or five voting members, in separate failure domains.
- What does each dependency do without a quorum? For etcd, a message broker, or a database replica set, find out which operations fail and which keep answering with possibly stale data. Check whether an acknowledged write can still roll back and how long failover takes with your timeouts. Then cut a node off in a test environment and watch.
- Are retries idempotent? Give each command a unique ID that every retry reuses, and have the state machine answer a repeated ID from its record instead of running the command again.
- Which reads need the latest write? Keep those linearizable, and allow possibly stale reads only where an old answer does no harm.
- Are your own leaders fenced? Pass a fencing token with every write a leader makes, and have each resource reject a token lower than the highest it has seen.
- What does “no quorum” mean to your callers? Decide in advance. For security data, refuse rather than guess, as the revocation check in my CAP article does.
Consensus buys one truth at the price of a quorum. Spend it where a single truth is genuinely required, and know what every system you depend on does when its majority is gone. A well-built one refuses to guess.
Further Reading
- In Search of an Understandable Consensus Algorithm (Extended Version) (Diego Ongaro and John Ousterhout, 20 May 2014)
- Consensus: Bridging Theory and Practice (Diego Ongaro, Stanford University, August 2014)
- Impossibility of Distributed Consensus with One Faulty Process (Michael J. Fischer, Nancy A. Lynch, and Michael S. Paterson, Journal of the ACM, April 1985)
- Coordination Avoidance in Database Systems (Peter Bailis and others, Proceedings of the VLDB Endowment, November 2014)
- Students’ Guide to Raft (Jon Gjengset, 16 March 2016)
- How to Do Distributed Locking (Martin Kleppmann, 8 February 2016)