How to implement distributed consensus in Rust?
Distributed consensus in Rust means guaranteeing that a set of processes agree on an ordered log of values despite failures. Practical implementation in Rust is dominated by one algorithm: Raft, due to its understandability and straightforward leader-based replication.
The most reliable path is not to write the protocol from scratch, but to integrate a mature crate with your application. The primary options are:
openraft– actively maintained fork ofraft-rs; modular, supports custom storage, networking, and hardening.raft-rs– port of the etcd Raft engine; stable but less flexible.async-raft– lightweight but largely superseded by the others.
Writing / embedding these libraries reduces your work to implementing four traits:
RaftTypeConfig, RaftLogStorage, RaftStateMachine, and RaftNetwork. That still requires you to supply a real network and storage backend, usually also called “implementing distributed consensus” in applied terms.
A production-grade custom Raft implementation in Rust would require:
- Persistent core state (current term, voted-for, log entries) stored with crash consistency (
fsyncbefore any response, usingsled,rocksdb, or a manual append-only WAL). - Volatile protocol state: commit index, last-applied, role (
Follower,Candidate,Leader). - An actor model — one async task per consensus node.
tokiois standard for timers and message demultiplexing. - Serializable RPC messages via
serde:RequestVote{RpcRequest, RpcResponse}andAppendEntries{RpcRequest, RpcResponse}(etcd naming), plusSnapshotmessages. - A network transport:
tokioover TCP/QUIC or a message broker, presenting a cleanSendRequest/HandleResponseinterface. The message direction distinguishes client requests from internal RPCs. - Timeout machinery: randomized election timeout (
tokio::time::sleepwithinselect!) and heartbeat interval. Precisely:- follower waits for AppendEntries,
- candidate waits either for quorum of votes or for timeout,
- leader heartbeats followers at fixed intervals.
- Log replication and application: leader appends entry, replicates to majority, commits, then applies to the state machine via an external command interface.
- Safety checks: term monotonicity; only one term voting decision; update
voted_forbefore granting vote; commit index only advances if the log is up-to-date with the leader.
An illustrative core skeleton (non-production but structurally correct):
use serde::{Serialize, Deserialize};
use tokio::sync::mpsc;
#[derive(Clone, Debug, PartialEq)]
enum Role { Follower, Candidate, Leader }
#[derive(Clone, Debug, Serialize, Deserialize)]
struct LogEntry { term: u64, command: Vec<u8> }
struct ConsensusNode {
id: u64,
peers: Vec<u64>,
role: Role,
persistent: PersistentState, // term, voted_for, log
commit_index: u64,
next_index: HashMap<u64, u64>, // per-peer
match_index: HashMap<u64, u64>,
state_machine: Option<Box<dyn StateMachine>>,
events: mpsc::UnboundedSender<Event>,
}
impl ConsensusNode {
async fn run(&mut self) {
// loop receives from network channel,
// handles RequestVote / AppendEntries,
// fires timers for elections and heartbeats.
}
}
What is often underestimated is testing consensus safety. A minimal correct implementation must also pass:
- single-node log replication,
- leader election under partitions,
- no log loss on leader crash,
- duplicate commands from retries.
Use loom for concurrency stress-testing and deterministic testing in tokio::time to simulate latency and network partitioning.
For systems aiming at non-Byzantine failures (crash, partition), Raft is sufficient. If nodes are suspected of actively malicious behavior (forking, lying), switch to a Byzantine fault-tolerant algorithm such as PBFT or Tendermint, for which tendermint-rs exists but is significantly more complex.
Final recommendation: start with openraft before building your own. The library already handles the immutable core invariants of Raft — terms, log matching, quorum math — and Rust’s type system lets you validate your own state transitions compile-time without sacrificing performance.