{"schemaVersion":1,"id":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped","title":"Consensus Algorithms Compared: Raft, Multi-Paxos, Viewstamped Replication, and EPaxos","description":"An implementation-focused comparison of distributed consensus algorithms, Raft, Multi-Paxos, Viewstamped Replication, and EPaxos, covering leader election, log compaction, liveness guarantees, and the nuances that only surface when you build them.","language":"en","datePublished":"2025-10-28","dateModified":"2026-11-10","contentFormat":"text/markdown","content":"Distributed Systems\n\n# Consensus Algorithms Compared: Raft, Multi-Paxos, Viewstamped Replication, and EPaxos\n\n2025-10-28Updated: 2026-11-1010 min read\n\n[Lucio Durán](<https://lucioduran.com/>)\nC.T.O. at LOi\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/consensus-algorithms-raft-paxos-viewstamped>)\n\n## Why Consensus Is Hard (A Quick Refresher)\n\nThe fundamental problem: N servers need to agree on a sequence of operations, even if up to F of them crash (where N \\>= 2F + 1). This agreement must be:\n\n- **Safe**: All non-faulty servers agree on the same sequence\n- **Live**: The system eventually makes progress (assuming fewer than F+1 crashes and the network eventually delivers messages)\n\nThe FLP impossibility result tells us we can't have both in an asynchronous network with even one crash failure. Every practical consensus algorithm works around this by using timeouts as failure detectors, which is technically violating the asynchronous model, but it works in practice because real networks have bounded-but-unknown delays.\n\n## Raft: The Algorithm That Explains Itself\n\nRaft was designed for understandability, and it delivers on that promise. The algorithm decomposes into three subproblems: leader election, log replication, and safety. Let me focus on the pieces that are harder to implement than the paper suggests.\n\n### Leader Election: The Subtle Parts\n\nThe basic mechanism is simple: candidates increment their term, vote for themselves, and request votes from others. But the implementation details matter enormously.\n\n```go\ntype RaftNode struct {\n mu sync.Mutex\n state NodeState\n currentTerm uint64\n votedFor *uint64\n log []LogEntry\n\n // Election timing - these values matter more than you think\n electionTimeout time.Duration\n heartbeatTimeout time.Duration\n lastHeartbeat time.Time\n\n // Pre-vote protocol (Section 9.6 of Raft dissertation)\n preVoteEnabled bool\n}\n\nfunc (n *RaftNode) startElection() {\n n.mu.Lock()\n defer n.mu.Unlock()\n\n if n.preVoteEnabled {\n // Pre-vote: ask peers if they'd vote for us WITHOUT incrementing term\n // This prevents disruption from partitioned nodes\n n.runPreVote()\n return\n }\n\n n.currentTerm++\n n.state = Candidate\n n.votedFor = &n.id\n n.persistState() // MUST be durable before sending RequestVote\n\n lastLogIndex, lastLogTerm := n.lastLogInfo()\n\n for _, peer := range n.peers {\n go func(p PeerID) {\n reply, err := n.sendRequestVote(p, &RequestVoteArgs{\n Term: n.currentTerm,\n CandidateID: n.id,\n LastLogIndex: lastLogIndex,\n LastLogTerm: lastLogTerm,\n })\n if err != nil {\n return\n }\n n.handleVoteResponse(reply)\n }(peer)\n }\n}\n```\n\nThe pre-vote protocol is critical and often omitted from tutorials. Without it, a node that gets partitioned from the cluster keeps incrementing its term. When it rejoins, its artificially high term forces a new election, disrupting the healthy cluster. Pre-vote asks \"would you vote for me?\" without actually incrementing the term. If the answer is no (because the cluster already has a healthy leader), the node backs off.\n\n### Log Compaction: Where The Pain Lives\n\nSnapshot installation and recovery need explicit handling beyond the log-compaction overview. The following code is an explanatory sketch.\n\n```go\ntype SnapshotManager struct {\n stateMachine StateMachine\n raftLog *RaftLog\n snapshotDir string\n\n // Concurrent snapshot support\n inProgress atomic.Bool\n lastIncluded struct {\n Index uint64\n Term uint64\n }\n}\n\nfunc (sm *SnapshotManager) TakeSnapshot() error {\n if !sm.inProgress.CompareAndSwap(false, true) {\n return ErrSnapshotInProgress\n }\n defer sm.inProgress.Store(false)\n\n // 1. Get a consistent point-in-time snapshot of the state machine\n // This MUST be atomic with respect to applied log entries\n snapshot, appliedIndex, appliedTerm := sm.stateMachine.Snapshot()\n\n // 2. Write to a temporary file first (crash safety)\n tmpPath := filepath.Join(sm.snapshotDir, fmt.Sprintf(\"snap-%d.tmp\", appliedIndex))\n finalPath := filepath.Join(sm.snapshotDir, fmt.Sprintf(\"snap-%d.dat\", appliedIndex))\n\n f, err := os.Create(tmpPath)\n if err != nil {\n return err\n }\n\n // 3. Write snapshot metadata + data\n header := SnapshotHeader{\n LastIncludedIndex: appliedIndex,\n LastIncludedTerm: appliedTerm,\n Size: uint64(len(snapshot)),\n Checksum: crc32.ChecksumIEEE(snapshot),\n }\n\n if err := binary.Write(f, binary.LittleEndian, header); err != nil {\n f.Close()\n os.Remove(tmpPath)\n return err\n }\n\n if _, err := f.Write(snapshot); err != nil {\n f.Close()\n os.Remove(tmpPath)\n return err\n }\n\n // 4. Fsync before rename (critical for crash safety)\n if err := f.Sync(); err != nil {\n f.Close()\n os.Remove(tmpPath)\n return err\n }\n f.Close()\n\n // 5. Atomic rename\n if err := os.Rename(tmpPath, finalPath); err != nil {\n return err\n }\n\n // 6. Truncate the log up to the snapshot point\n sm.raftLog.TruncateBefore(appliedIndex)\n\n sm.lastIncluded.Index = appliedIndex\n sm.lastIncluded.Term = appliedTerm\n\n return nil\n}\n```\n\nThe tricky parts:\n\n1. **Taking the snapshot must not block log application.** If your state machine is a B-tree, you need either copy-on-write semantics or a snapshot isolation mechanism. BoltDB gives you this for free; a naive in-memory map doesn't.\n2. **InstallSnapshot RPC is chunked.** When a follower is so far behind that the leader has already compacted its log, the leader sends its snapshot. For a multi-GB state machine, this must be chunked and resumable. The paper mentions this in one sentence.\n3. **Crash recovery interleaves snapshots and logs.** On restart, you load the latest snapshot, then replay log entries after the snapshot's last included index. If you crashed between taking a snapshot and truncating the log, you might have overlapping entries. Your recovery logic needs to handle this.\n\n## Multi-Paxos: A Protocol Without a Canonical Specification\n\nHere's the dirty secret about Multi-Paxos: there is no canonical Multi-Paxos paper. Lamport's original \"The Part-Time Parliament\" describes single-decree Paxos. The extension to multi-decree (a sequence of consensus instances) is described informally, and every implementation makes different choices.\n\nThe core optimization of Multi-Paxos over basic Paxos: once a leader is established, it can skip the Prepare phase for subsequent log entries, going straight to Accept. This means steady-state operations only require one round-trip instead of two.\n\n```go\ntype MultiPaxosNode struct {\n proposerState struct {\n ballotNum BallotNumber\n isLeader bool\n maxSlot uint64\n // Per-slot state for the accept phase\n accepts map[uint64]*AcceptState\n }\n\n acceptorState struct {\n promisedBallot BallotNumber\n // Per-slot: highest accepted ballot and value\n accepted map[uint64]AcceptedEntry\n }\n\n learnerState struct {\n chosen map[uint64]ChosenEntry\n }\n}\n\nfunc (n *MultiPaxosNode) Propose(value []byte) (uint64, error) {\n if !n.proposerState.isLeader {\n return 0, ErrNotLeader\n }\n\n slot := atomic.AddUint64(&n.proposerState.maxSlot, 1)\n\n // As established leader, skip Phase 1 (Prepare)\n // Go directly to Phase 2 (Accept)\n acceptMsg := &AcceptMessage{\n Ballot: n.proposerState.ballotNum,\n Slot: slot,\n Value: value,\n }\n\n ackCount := 1 // Count self\n quorum := (len(n.peers) / 2) + 1\n\n for _, peer := range n.peers {\n go func(p PeerID) {\n reply, err := n.sendAccept(p, acceptMsg)\n if err != nil {\n return\n }\n\n if reply.Ballot > n.proposerState.ballotNum {\n // Someone else has a higher ballot - we're no longer leader\n n.stepDown(reply.Ballot)\n return\n }\n\n if atomic.AddInt32(&ackCount, 1) >= int32(quorum) {\n n.markChosen(slot, value)\n }\n }(peer)\n }\n\n return slot, nil\n}\n```\n\n### The Slot Gap Problem\n\nIn Multi-Paxos, slots can be chosen out of order. Client A's request might get slot 5, and Client B's request might get slot 7, while slot 6 is still pending. When delivering commands to the state machine, you must deliver in order. This means you need a mechanism to fill gaps, either with no-ops or by re-proposing values for empty slots.\n\nThis gap-filling logic is where most Multi-Paxos implementations have bugs. The interaction between gap-filling, leader changes, and the accept phase is subtle. If a new leader starts filling gaps concurrently with the old leader's accepts still in flight, you can end up with a slot being chosen with two different values unless the ballot number mechanism is correctly implemented.\n\n## Viewstamped Replication: The Forgotten Middle Child\n\nVR predates Paxos in publication and is remarkably similar to Raft in structure, it has a primary (leader), views (terms), and a log. The key differences are in the view change protocol and the reconfiguration mechanism.\n\n```\nNormal Operation (simplified):\n1. Client → Primary: REQUEST(op, client-id, request-num)\n2. Primary: Assign op-number, append to log\n3. Primary → Backups: PREPARE(view, op-number, op, commit-number)\n4. Backups: Append to log, reply PREPARE-OK\n5. Primary: After f+1 PREPARE-OKs, commit\n6. Primary → Client: REPLY(view, request-num, result)\n```\n\nThe interesting thing about VR is its view change protocol, which is more structured than Raft's election. When a backup suspects the primary has failed:\n\n1. It sends ` START-VIEW-CHANGE ` messages\n2. After receiving f such messages, a node sends ` DO-VIEW-CHANGE ` to the new primary (determined by round-robin)\n3. The new primary collects f+1 ` DO-VIEW-CHANGE ` messages, selects the log with the latest data, and starts the new view\n\nThe deterministic primary selection (round-robin based on view number) means there's no split-vote scenario, you don't need randomized timeouts like Raft. The tradeoff is less flexibility: you can't prefer a node with better hardware or lower latency to clients.\n\n### Where VR Shines\n\nVR's reconfiguration protocol (changing the set of replicas) is significantly simpler than Raft's joint consensus approach. In VR, reconfiguration is just a special operation in the log. When it commits, the new configuration takes effect at that point in the log. Old replicas transfer state to new replicas, and the transition is clean.\n\n## EPaxos: The Leaderless Promise\n\nEPaxos (Egalitarian Paxos) is the most intellectually ambitious of the four. Instead of funneling all operations through a leader, any replica can propose commands. Non-conflicting commands can be committed in a single round-trip (the fast path), and conflicting commands are resolved through a dependency graph.\n\n### The Dependency Graph\n\nThis is where EPaxos gets complicated. Every command carries a set of dependencies, other commands it must be executed after:\n\n```go\ntype EPaxosInstance struct {\n Command []byte\n Deps map[InstanceID]uint64 // dependency → sequence number\n SeqNum uint64\n Status InstanceStatus // PreAccepted, Accepted, Committed, Executed\n Ballot BallotNumber\n}\n\nfunc (r *EPaxosReplica) StartCommand(cmd []byte) {\n inst := &EPaxosInstance{\n Command: cmd,\n SeqNum: 0,\n Deps: make(map[InstanceID]uint64),\n Status: PreAccepted,\n }\n\n // Calculate initial dependencies and sequence number\n for id, other := range r.instances {\n if r.conflictsWithAny(cmd, other.Command) {\n inst.Deps[id] = other.SeqNum\n if other.SeqNum >= inst.SeqNum {\n inst.SeqNum = other.SeqNum + 1\n }\n }\n }\n\n // Fast path: send PreAccept to a super-quorum\n superQuorum := (3*len(r.peers)/4) + 1\n replies := r.broadcastPreAccept(inst)\n\n if r.fastPathSucceeded(replies, inst, superQuorum) {\n // All replicas agreed on the same dependencies\n inst.Status = Committed\n r.broadcastCommit(inst)\n } else {\n // Slow path: need explicit Accept phase\n r.mergedDeps(inst, replies)\n inst.Status = Accepted\n r.broadcastAccept(inst)\n // Then commit after majority ack\n }\n}\n```\n\n### The Execution Problem\n\nCommitting is easy. Executing is hard. Because commands are committed by different replicas with different dependency sets, you need a deterministic algorithm to linearize the dependency graph. This uses Tarjan's strongly connected components algorithm:\n\n1. Build the dependency graph from committed instances\n2. Find all strongly connected components (SCCs)\n3. Execute SCCs in reverse topological order\n4. Within each SCC, execute commands in sequence number order\n\nThis means you can't execute a command the moment it's committed, you have to wait until all its dependencies are also committed, and then compute the execution order. In practice, this adds latency variance that the fast-path savings don't always compensate for.\n\n## Comparing implementations\n\nProtocol names alone do not determine performance. Compare concrete implementations under the same durability settings, workload, conflict rate, batching policy and network conditions. Publish the harness, versions and repeated results before drawing numerical conclusions. No reproducible benchmark is supplied here.\n\n## What I'd Choose Today\n\nFor most systems: **Raft with etcd's optimizations** (pipelining, learner nodes, pre-vote, leader lease for reads). The ecosystem is unmatched, [etcd](<https://etcd.io/>), [CockroachDB](<https://www.cockroachlabs.com/>), [TiKV](<https://tikv.org/>), and [Consul](<https://www.consul.io/>) all use Raft variants, and their battle-tested implementations are available as libraries.\n\nFor geo-distributed systems with mostly non-conflicting operations: **EPaxos**, but only if you have the engineering resources to handle the implementation complexity and the debugging nightmare that is dependency graph resolution.\n\nFor learning and building intuition: implement **Raft first, then VR**. The similarities will crystallize your understanding, and the differences in view change vs. election will teach you why these design choices matter.\n\nMulti-Paxos is the right answer if you need maximum throughput on a single leader and you're willing to invest in a custom implementation. But honestly, at that point, you should probably just use etcd or CockroachDB and focus your engineering effort on your actual product.\n\n## Editorial method and scope\n\nReviewed on November 10, 2026. Unsupported claims of personal production experience and numerical results were removed. The remaining examples explain techniques; their presence is not a claim that they were executed in this review. This revision is not a complete validation of every API, version or code sample in the article.\n\n## Frequently asked questions\n\n### Which consensus algorithm should I use for a new project?\n\nRaft, unless you have a specific reason not to. It's the most well-understood, has the most production implementations, the best debugging tooling, and the clearest specification. Multi-Paxos can be more efficient for high-throughput workloads, but the implementation complexity is significantly higher.\n\n### Is EPaxos actually better than Raft in practice?\n\nIt depends on your workload. EPaxos eliminates the leader bottleneck and achieves lower latency for non-conflicting commands in geo-distributed deployments. But the implementation complexity is an order of magnitude higher, the recovery protocol is notoriously difficult to get right, and conflicting commands can cause significant performance degradation.\n\n### What's the difference between Paxos and Multi-Paxos?\n\nClassic Paxos is a protocol for agreeing on a single value. Multi-Paxos extends it to a sequence of values (a replicated log) by electing a stable leader that can skip the prepare phase for subsequent log entries, amortizing the cost of leader election across many operations.\n\n## Tips\n\n1. **LuchoTip:** Raft's leader lease optimization (where the leader serves reads without quorum) breaks linearizability if clock skew exceeds the lease duration. Always use ` ReadIndex ` or ` ReadOnlySafe ` for reads that must be linearizable.\n2. **LuchoTip:** When implementing log compaction in Raft, snapshot the state machine on a separate goroutine/thread and use a copy-on-write data structure. Blocking the main loop during snapshotting causes election timeouts in large clusters.\n3. **LuchoTip:** EPaxos's fast-path requires a super-quorum (⌊3F/4⌋+1 for F failures). In a 5-node cluster, that's 4 out of 5 nodes. One slow node kills your fast path, which is why EPaxos performs worse than Raft in heterogeneous networks despite looking better on paper.\n4. **LuchoTip:** For Multi-Paxos, the distinction between ` prepare ` and ` accept ` phases is where most bugs hide. If your leader crashes between sending accepts and receiving a quorum of acks, the new leader's prepare phase MUST discover those in-flight accepts. Use a persistent log, not an in-memory map.\n\n- raft\n- paxos\n- epaxos\n- consensus\n- distributed-systems\n- viewstamped-replication\n- leader-election\n- log-replication\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"Consensus Algorithms Compared: Raft, Multi-Paxos, Viewstamped Replication, and EPaxos","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#title"},{"level":2,"text":"Why Consensus Is Hard (A Quick Refresher)","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#why-consensus-is-hard-a-quick-refresher"},{"level":2,"text":"Raft: The Algorithm That Explains Itself","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#raft-the-algorithm-that-explains-itself"},{"level":3,"text":"Leader Election: The Subtle Parts","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#leader-election-the-subtle-parts"},{"level":3,"text":"Log Compaction: Where The Pain Lives","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#log-compaction-where-the-pain-lives"},{"level":2,"text":"Multi-Paxos: A Protocol Without a Canonical Specification","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#multi-paxos-a-protocol-without-a-canonical-specification"},{"level":3,"text":"The Slot Gap Problem","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#the-slot-gap-problem"},{"level":2,"text":"Viewstamped Replication: The Forgotten Middle Child","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#viewstamped-replication-the-forgotten-middle-child"},{"level":3,"text":"Where VR Shines","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#where-vr-shines"},{"level":2,"text":"EPaxos: The Leaderless Promise","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#epaxos-the-leaderless-promise"},{"level":3,"text":"The Dependency Graph","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#the-dependency-graph"},{"level":3,"text":"The Execution Problem","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#the-execution-problem"},{"level":2,"text":"Comparing implementations","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#comparing-implementations"},{"level":2,"text":"What I'd Choose Today","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#what-id-choose-today"},{"level":2,"text":"Editorial method and scope","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#editorial-method-and-scope"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#questions"},{"level":3,"text":"Which consensus algorithm should I use for a new project?","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#question-1"},{"level":3,"text":"Is EPaxos actually better than Raft in practice?","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#question-2"},{"level":3,"text":"What's the difference between Paxos and Multi-Paxos?","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#article-tips"}],"sections":[{"heading":"Consensus Algorithms Compared: Raft, Multi-Paxos, Viewstamped Replication, and EPaxos","level":1,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped","content":"Distributed Systems"},{"heading":"Consensus Algorithms Compared: Raft, Multi-Paxos, Viewstamped Replication, and EPaxos","level":1,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#title","content":"2025-10-28Updated: 2026-11-1010 min read\n\n[Lucio Durán](<https://lucioduran.com/>)\nC.T.O. at LOi\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/consensus-algorithms-raft-paxos-viewstamped>)"},{"heading":"Why Consensus Is Hard (A Quick Refresher)","level":2,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#why-consensus-is-hard-a-quick-refresher","content":"The fundamental problem: N servers need to agree on a sequence of operations, even if up to F of them crash (where N \\>= 2F + 1). This agreement must be:\n\n- **Safe**: All non-faulty servers agree on the same sequence\n- **Live**: The system eventually makes progress (assuming fewer than F+1 crashes and the network eventually delivers messages)\n\nThe FLP impossibility result tells us we can't have both in an asynchronous network with even one crash failure. Every practical consensus algorithm works around this by using timeouts as failure detectors, which is technically violating the asynchronous model, but it works in practice because real networks have bounded-but-unknown delays."},{"heading":"Raft: The Algorithm That Explains Itself","level":2,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#raft-the-algorithm-that-explains-itself","content":"Raft was designed for understandability, and it delivers on that promise. The algorithm decomposes into three subproblems: leader election, log replication, and safety. Let me focus on the pieces that are harder to implement than the paper suggests."},{"heading":"Leader Election: The Subtle Parts","level":3,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#leader-election-the-subtle-parts","content":"The basic mechanism is simple: candidates increment their term, vote for themselves, and request votes from others. But the implementation details matter enormously.\n\n```go\ntype RaftNode struct {\n mu sync.Mutex\n state NodeState\n currentTerm uint64\n votedFor *uint64\n log []LogEntry\n\n // Election timing - these values matter more than you think\n electionTimeout time.Duration\n heartbeatTimeout time.Duration\n lastHeartbeat time.Time\n\n // Pre-vote protocol (Section 9.6 of Raft dissertation)\n preVoteEnabled bool\n}\n\nfunc (n *RaftNode) startElection() {\n n.mu.Lock()\n defer n.mu.Unlock()\n\n if n.preVoteEnabled {\n // Pre-vote: ask peers if they'd vote for us WITHOUT incrementing term\n // This prevents disruption from partitioned nodes\n n.runPreVote()\n return\n }\n\n n.currentTerm++\n n.state = Candidate\n n.votedFor = &n.id\n n.persistState() // MUST be durable before sending RequestVote\n\n lastLogIndex, lastLogTerm := n.lastLogInfo()\n\n for _, peer := range n.peers {\n go func(p PeerID) {\n reply, err := n.sendRequestVote(p, &RequestVoteArgs{\n Term: n.currentTerm,\n CandidateID: n.id,\n LastLogIndex: lastLogIndex,\n LastLogTerm: lastLogTerm,\n })\n if err != nil {\n return\n }\n n.handleVoteResponse(reply)\n }(peer)\n }\n}\n```\n\nThe pre-vote protocol is critical and often omitted from tutorials. Without it, a node that gets partitioned from the cluster keeps incrementing its term. When it rejoins, its artificially high term forces a new election, disrupting the healthy cluster. Pre-vote asks \"would you vote for me?\" without actually incrementing the term. If the answer is no (because the cluster already has a healthy leader), the node backs off."},{"heading":"Log Compaction: Where The Pain Lives","level":3,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#log-compaction-where-the-pain-lives","content":"Snapshot installation and recovery need explicit handling beyond the log-compaction overview. The following code is an explanatory sketch.\n\n```go\ntype SnapshotManager struct {\n stateMachine StateMachine\n raftLog *RaftLog\n snapshotDir string\n\n // Concurrent snapshot support\n inProgress atomic.Bool\n lastIncluded struct {\n Index uint64\n Term uint64\n }\n}\n\nfunc (sm *SnapshotManager) TakeSnapshot() error {\n if !sm.inProgress.CompareAndSwap(false, true) {\n return ErrSnapshotInProgress\n }\n defer sm.inProgress.Store(false)\n\n // 1. Get a consistent point-in-time snapshot of the state machine\n // This MUST be atomic with respect to applied log entries\n snapshot, appliedIndex, appliedTerm := sm.stateMachine.Snapshot()\n\n // 2. Write to a temporary file first (crash safety)\n tmpPath := filepath.Join(sm.snapshotDir, fmt.Sprintf(\"snap-%d.tmp\", appliedIndex))\n finalPath := filepath.Join(sm.snapshotDir, fmt.Sprintf(\"snap-%d.dat\", appliedIndex))\n\n f, err := os.Create(tmpPath)\n if err != nil {\n return err\n }\n\n // 3. Write snapshot metadata + data\n header := SnapshotHeader{\n LastIncludedIndex: appliedIndex,\n LastIncludedTerm: appliedTerm,\n Size: uint64(len(snapshot)),\n Checksum: crc32.ChecksumIEEE(snapshot),\n }\n\n if err := binary.Write(f, binary.LittleEndian, header); err != nil {\n f.Close()\n os.Remove(tmpPath)\n return err\n }\n\n if _, err := f.Write(snapshot); err != nil {\n f.Close()\n os.Remove(tmpPath)\n return err\n }\n\n // 4. Fsync before rename (critical for crash safety)\n if err := f.Sync(); err != nil {\n f.Close()\n os.Remove(tmpPath)\n return err\n }\n f.Close()\n\n // 5. Atomic rename\n if err := os.Rename(tmpPath, finalPath); err != nil {\n return err\n }\n\n // 6. Truncate the log up to the snapshot point\n sm.raftLog.TruncateBefore(appliedIndex)\n\n sm.lastIncluded.Index = appliedIndex\n sm.lastIncluded.Term = appliedTerm\n\n return nil\n}\n```\n\nThe tricky parts:\n\n1. **Taking the snapshot must not block log application.** If your state machine is a B-tree, you need either copy-on-write semantics or a snapshot isolation mechanism. BoltDB gives you this for free; a naive in-memory map doesn't.\n2. **InstallSnapshot RPC is chunked.** When a follower is so far behind that the leader has already compacted its log, the leader sends its snapshot. For a multi-GB state machine, this must be chunked and resumable. The paper mentions this in one sentence.\n3. **Crash recovery interleaves snapshots and logs.** On restart, you load the latest snapshot, then replay log entries after the snapshot's last included index. If you crashed between taking a snapshot and truncating the log, you might have overlapping entries. Your recovery logic needs to handle this."},{"heading":"Multi-Paxos: A Protocol Without a Canonical Specification","level":2,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#multi-paxos-a-protocol-without-a-canonical-specification","content":"Here's the dirty secret about Multi-Paxos: there is no canonical Multi-Paxos paper. Lamport's original \"The Part-Time Parliament\" describes single-decree Paxos. The extension to multi-decree (a sequence of consensus instances) is described informally, and every implementation makes different choices.\n\nThe core optimization of Multi-Paxos over basic Paxos: once a leader is established, it can skip the Prepare phase for subsequent log entries, going straight to Accept. This means steady-state operations only require one round-trip instead of two.\n\n```go\ntype MultiPaxosNode struct {\n proposerState struct {\n ballotNum BallotNumber\n isLeader bool\n maxSlot uint64\n // Per-slot state for the accept phase\n accepts map[uint64]*AcceptState\n }\n\n acceptorState struct {\n promisedBallot BallotNumber\n // Per-slot: highest accepted ballot and value\n accepted map[uint64]AcceptedEntry\n }\n\n learnerState struct {\n chosen map[uint64]ChosenEntry\n }\n}\n\nfunc (n *MultiPaxosNode) Propose(value []byte) (uint64, error) {\n if !n.proposerState.isLeader {\n return 0, ErrNotLeader\n }\n\n slot := atomic.AddUint64(&n.proposerState.maxSlot, 1)\n\n // As established leader, skip Phase 1 (Prepare)\n // Go directly to Phase 2 (Accept)\n acceptMsg := &AcceptMessage{\n Ballot: n.proposerState.ballotNum,\n Slot: slot,\n Value: value,\n }\n\n ackCount := 1 // Count self\n quorum := (len(n.peers) / 2) + 1\n\n for _, peer := range n.peers {\n go func(p PeerID) {\n reply, err := n.sendAccept(p, acceptMsg)\n if err != nil {\n return\n }\n\n if reply.Ballot > n.proposerState.ballotNum {\n // Someone else has a higher ballot - we're no longer leader\n n.stepDown(reply.Ballot)\n return\n }\n\n if atomic.AddInt32(&ackCount, 1) >= int32(quorum) {\n n.markChosen(slot, value)\n }\n }(peer)\n }\n\n return slot, nil\n}\n```"},{"heading":"The Slot Gap Problem","level":3,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#the-slot-gap-problem","content":"In Multi-Paxos, slots can be chosen out of order. Client A's request might get slot 5, and Client B's request might get slot 7, while slot 6 is still pending. When delivering commands to the state machine, you must deliver in order. This means you need a mechanism to fill gaps, either with no-ops or by re-proposing values for empty slots.\n\nThis gap-filling logic is where most Multi-Paxos implementations have bugs. The interaction between gap-filling, leader changes, and the accept phase is subtle. If a new leader starts filling gaps concurrently with the old leader's accepts still in flight, you can end up with a slot being chosen with two different values unless the ballot number mechanism is correctly implemented."},{"heading":"Viewstamped Replication: The Forgotten Middle Child","level":2,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#viewstamped-replication-the-forgotten-middle-child","content":"VR predates Paxos in publication and is remarkably similar to Raft in structure, it has a primary (leader), views (terms), and a log. The key differences are in the view change protocol and the reconfiguration mechanism.\n\n```\nNormal Operation (simplified):\n1. Client → Primary: REQUEST(op, client-id, request-num)\n2. Primary: Assign op-number, append to log\n3. Primary → Backups: PREPARE(view, op-number, op, commit-number)\n4. Backups: Append to log, reply PREPARE-OK\n5. Primary: After f+1 PREPARE-OKs, commit\n6. Primary → Client: REPLY(view, request-num, result)\n```\n\nThe interesting thing about VR is its view change protocol, which is more structured than Raft's election. When a backup suspects the primary has failed:\n\n1. It sends ` START-VIEW-CHANGE ` messages\n2. After receiving f such messages, a node sends ` DO-VIEW-CHANGE ` to the new primary (determined by round-robin)\n3. The new primary collects f+1 ` DO-VIEW-CHANGE ` messages, selects the log with the latest data, and starts the new view\n\nThe deterministic primary selection (round-robin based on view number) means there's no split-vote scenario, you don't need randomized timeouts like Raft. The tradeoff is less flexibility: you can't prefer a node with better hardware or lower latency to clients."},{"heading":"Where VR Shines","level":3,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#where-vr-shines","content":"VR's reconfiguration protocol (changing the set of replicas) is significantly simpler than Raft's joint consensus approach. In VR, reconfiguration is just a special operation in the log. When it commits, the new configuration takes effect at that point in the log. Old replicas transfer state to new replicas, and the transition is clean."},{"heading":"EPaxos: The Leaderless Promise","level":2,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#epaxos-the-leaderless-promise","content":"EPaxos (Egalitarian Paxos) is the most intellectually ambitious of the four. Instead of funneling all operations through a leader, any replica can propose commands. Non-conflicting commands can be committed in a single round-trip (the fast path), and conflicting commands are resolved through a dependency graph."},{"heading":"The Dependency Graph","level":3,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#the-dependency-graph","content":"This is where EPaxos gets complicated. Every command carries a set of dependencies, other commands it must be executed after:\n\n```go\ntype EPaxosInstance struct {\n Command []byte\n Deps map[InstanceID]uint64 // dependency → sequence number\n SeqNum uint64\n Status InstanceStatus // PreAccepted, Accepted, Committed, Executed\n Ballot BallotNumber\n}\n\nfunc (r *EPaxosReplica) StartCommand(cmd []byte) {\n inst := &EPaxosInstance{\n Command: cmd,\n SeqNum: 0,\n Deps: make(map[InstanceID]uint64),\n Status: PreAccepted,\n }\n\n // Calculate initial dependencies and sequence number\n for id, other := range r.instances {\n if r.conflictsWithAny(cmd, other.Command) {\n inst.Deps[id] = other.SeqNum\n if other.SeqNum >= inst.SeqNum {\n inst.SeqNum = other.SeqNum + 1\n }\n }\n }\n\n // Fast path: send PreAccept to a super-quorum\n superQuorum := (3*len(r.peers)/4) + 1\n replies := r.broadcastPreAccept(inst)\n\n if r.fastPathSucceeded(replies, inst, superQuorum) {\n // All replicas agreed on the same dependencies\n inst.Status = Committed\n r.broadcastCommit(inst)\n } else {\n // Slow path: need explicit Accept phase\n r.mergedDeps(inst, replies)\n inst.Status = Accepted\n r.broadcastAccept(inst)\n // Then commit after majority ack\n }\n}\n```"},{"heading":"The Execution Problem","level":3,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#the-execution-problem","content":"Committing is easy. Executing is hard. Because commands are committed by different replicas with different dependency sets, you need a deterministic algorithm to linearize the dependency graph. This uses Tarjan's strongly connected components algorithm:\n\n1. Build the dependency graph from committed instances\n2. Find all strongly connected components (SCCs)\n3. Execute SCCs in reverse topological order\n4. Within each SCC, execute commands in sequence number order\n\nThis means you can't execute a command the moment it's committed, you have to wait until all its dependencies are also committed, and then compute the execution order. In practice, this adds latency variance that the fast-path savings don't always compensate for."},{"heading":"Comparing implementations","level":2,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#comparing-implementations","content":"Protocol names alone do not determine performance. Compare concrete implementations under the same durability settings, workload, conflict rate, batching policy and network conditions. Publish the harness, versions and repeated results before drawing numerical conclusions. No reproducible benchmark is supplied here."},{"heading":"What I'd Choose Today","level":2,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#what-id-choose-today","content":"For most systems: **Raft with etcd's optimizations** (pipelining, learner nodes, pre-vote, leader lease for reads). The ecosystem is unmatched, [etcd](<https://etcd.io/>), [CockroachDB](<https://www.cockroachlabs.com/>), [TiKV](<https://tikv.org/>), and [Consul](<https://www.consul.io/>) all use Raft variants, and their battle-tested implementations are available as libraries.\n\nFor geo-distributed systems with mostly non-conflicting operations: **EPaxos**, but only if you have the engineering resources to handle the implementation complexity and the debugging nightmare that is dependency graph resolution.\n\nFor learning and building intuition: implement **Raft first, then VR**. The similarities will crystallize your understanding, and the differences in view change vs. election will teach you why these design choices matter.\n\nMulti-Paxos is the right answer if you need maximum throughput on a single leader and you're willing to invest in a custom implementation. But honestly, at that point, you should probably just use etcd or CockroachDB and focus your engineering effort on your actual product."},{"heading":"Editorial method and scope","level":2,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#editorial-method-and-scope","content":"Reviewed on November 10, 2026. Unsupported claims of personal production experience and numerical results were removed. The remaining examples explain techniques; their presence is not a claim that they were executed in this review. This revision is not a complete validation of every API, version or code sample in the article."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#questions","content":""},{"heading":"Which consensus algorithm should I use for a new project?","level":3,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#question-1","content":"Raft, unless you have a specific reason not to. It's the most well-understood, has the most production implementations, the best debugging tooling, and the clearest specification. Multi-Paxos can be more efficient for high-throughput workloads, but the implementation complexity is significantly higher."},{"heading":"Is EPaxos actually better than Raft in practice?","level":3,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#question-2","content":"It depends on your workload. EPaxos eliminates the leader bottleneck and achieves lower latency for non-conflicting commands in geo-distributed deployments. But the implementation complexity is an order of magnitude higher, the recovery protocol is notoriously difficult to get right, and conflicting commands can cause significant performance degradation."},{"heading":"What's the difference between Paxos and Multi-Paxos?","level":3,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#question-3","content":"Classic Paxos is a protocol for agreeing on a single value. Multi-Paxos extends it to a sequence of values (a replicated log) by electing a stable leader that can skip the prepare phase for subsequent log entries, amortizing the cost of leader election across many operations."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#article-tips","content":"1. **LuchoTip:** Raft's leader lease optimization (where the leader serves reads without quorum) breaks linearizability if clock skew exceeds the lease duration. Always use ` ReadIndex ` or ` ReadOnlySafe ` for reads that must be linearizable.\n2. **LuchoTip:** When implementing log compaction in Raft, snapshot the state machine on a separate goroutine/thread and use a copy-on-write data structure. Blocking the main loop during snapshotting causes election timeouts in large clusters.\n3. **LuchoTip:** EPaxos's fast-path requires a super-quorum (⌊3F/4⌋+1 for F failures). In a 5-node cluster, that's 4 out of 5 nodes. One slow node kills your fast path, which is why EPaxos performs worse than Raft in heterogeneous networks despite looking better on paper.\n4. **LuchoTip:** For Multi-Paxos, the distinction between ` prepare ` and ` accept ` phases is where most bugs hide. If your leader crashes between sending accepts and receiving a quorum of acks, the new leader's prepare phase MUST discover those in-flight accepts. Use a persistent log, not an in-memory map.\n\n- raft\n- paxos\n- epaxos\n- consensus\n- distributed-systems\n- viewstamped-replication\n- leader-election\n- log-replication\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Lucio Durán","url":"https://lucioduran.com/"},{"text":"Español","url":"https://lucioduran.com/es/blog/consensus-algorithms-raft-paxos-viewstamped"},{"text":"etcd","url":"https://etcd.io/"},{"text":"CockroachDB","url":"https://www.cockroachlabs.com/"},{"text":"TiKV","url":"https://tikv.org/"},{"text":"Consul","url":"https://www.consul.io/"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com/"},{"@type":"WebSite","@id":"https://lucioduran.com/#website","url":"https://lucioduran.com/","name":"Lucio Durán","inLanguage":["en","es"],"publisher":{"@id":"https://lucioduran.com/#person"}},{"@type":"WebPage","@id":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#page","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped","name":"Consensus Algorithms Compared: Raft, Multi-Paxos, Viewstamped Replication, and EPaxos | Lucio Durán","description":"An implementation-focused comparison of distributed consensus algorithms, Raft, Multi-Paxos, Viewstamped Replication, and EPaxos, covering leader election, log compaction, liveness guarantees, and the nuances that only surface when you build them.","inLanguage":"en","author":{"@id":"https://lucioduran.com/#person"},"publisher":{"@id":"https://lucioduran.com/#person"},"isPartOf":{"@id":"https://lucioduran.com/#website"},"isAccessibleForFree":true,"datePublished":"2025-10-28","dateModified":"2026-11-10","mainEntity":{"@id":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#article"},"breadcrumb":{"@id":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#breadcrumb"}},{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#article","mainEntityOfPage":{"@id":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#page"},"headline":"Consensus Algorithms Compared: Raft, Multi-Paxos, Viewstamped Replication, and EPaxos","description":"An implementation-focused comparison of distributed consensus algorithms, Raft, Multi-Paxos, Viewstamped Replication, and EPaxos, covering leader election, log compaction, liveness guarantees, and the nuances that only surface when you build them.","datePublished":"2025-10-28","dateModified":"2026-11-10","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@id":"https://lucioduran.com/#person"},"url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Distributed Systems","keywords":"raft, paxos, epaxos, consensus, distributed-systems, viewstamped-replication, leader-election, log-replication","wordCount":1912,"timeRequired":"PT10M","workTranslation":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/consensus-algorithms-raft-paxos-viewstamped#article","inLanguage":"es","url":"https://lucioduran.com/es/blog/consensus-algorithms-raft-paxos-viewstamped"}],"isAccessibleForFree":true},{"@type":"Blog","@id":"https://lucioduran.com/blog#blog","url":"https://lucioduran.com/blog","name":"Blog","inLanguage":"en","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com/"},"publisher":{"@id":"https://lucioduran.com/#person"},"isPartOf":{"@id":"https://lucioduran.com/#website"}},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"Consensus Algorithms Compared: Raft, Multi-Paxos, Viewstamped Replication, and EPaxos","item":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped"}],"@id":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#breadcrumb"},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Which consensus algorithm should I use for a new project?","acceptedAnswer":{"@type":"Answer","text":"Raft, unless you have a specific reason not to. It's the most well-understood, has the most production implementations, the best debugging tooling, and the clearest specification. Multi-Paxos can be more efficient for high-throughput workloads, but the implementation complexity is significantly higher."}},{"@type":"Question","name":"Is EPaxos actually better than Raft in practice?","acceptedAnswer":{"@type":"Answer","text":"It depends on your workload. EPaxos eliminates the leader bottleneck and achieves lower latency for non-conflicting commands in geo-distributed deployments. But the implementation complexity is an order of magnitude higher, the recovery protocol is notoriously difficult to get right, and conflicting commands can cause significant performance degradation."}},{"@type":"Question","name":"What's the difference between Paxos and Multi-Paxos?","acceptedAnswer":{"@type":"Answer","text":"Classic Paxos is a protocol for agreeing on a single value. Multi-Paxos extends it to a sequence of values (a replicated log) by electing a stable leader that can skip the prepare phase for subsequent log entries, amortizing the cost of leader election across many operations."}}],"@id":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#questions","url":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#questions","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped#page"},"author":{"@id":"https://lucioduran.com/#person"}}]}],"contentHash":"025cd550cb6bebb3bf510be86e42691b4a607f6931fa70cb0096f66f4e1aa2c5","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/consensus-algorithms-raft-paxos-viewstamped","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":80970,"contentBytes":16598}}
