etcd-io/raft: the consensus algorithm without storage, transport or a state machine
Raft library for maintaining a replicated state machine
At a glance
- What is it?
- etcd-io/raft implements only the Raft algorithm and leaves network transport and durable storage to the user, which keeps the core deterministic and testable. This looks at the Node API, the Ready contract that callers must honour, and the read paths that differ by clock assumptions.
- Who is it for?
- etcd-io/raft fits a team building a replicated state machine that already has, or wants to own, its network layer and its durable log, and that values a small deterministic core it can test with data driven and TLA+ based tests. It does not fit a team looking for a database, a queue, or a cluster to run, since the library ships none of those, and it is the wrong choice if you are not prepared to implement persistence and message transport yourself.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The library implements the algorithm and nothing else
Raft is a protocol with which a cluster of nodes can maintain a replicated state machine, kept in sync through a replicated log. The reference for the algorithm is the original paper, In Search of an Understandable Consensus Algorithm, by Diego Ongaro and John Ousterhout.
What the etcd-io/raft repository contains is the algorithm and not much else. The project documentation contrasts this with most Raft implementations, which have a monolithic design that includes storage handling, messaging serialization and network transport. This library follows a minimalistic design philosophy by only implementing the core Raft algorithm, and the documentation argues that the minimalism buys flexibility, determinism and performance.
The consequence for the user is stated plainly: the library only implements the Raft algorithm, both network and disk IO are left to the user, and users must implement their own transportation layer for message passing between Raft peers over the wire, as well as their own storage layer to persist the Raft log and state.
That is a genuine trade, and it is the whole trade. You get a core with no opinions about your wire format or your disk, and you give up the ready-made thing that would have saved you a project. The project describes itself as stable and feature complete, and it is the library behind etcd, Kubernetes, Docker Swarm, CockroachDB, TiDB, Flannel and others, listed in the documentation under notable users along with dgraph, TiKV, swarmkit and chain core.
Raft as a state machine, which is what makes it testable
The design decision that makes the library testable is that Raft is modelled as a state machine. The state machine takes a `Message` as input, and a message is either a local timer update or a network message sent from a remote peer. The state machine's output is a three-tuple of an array of `Messages`, log entries, and Raft state changes.
The property the documentation demands is that for state machines with the same state, the same state machine input should always generate the same state machine output. That is determinism, and it is what lets the repository test the algorithm with data driven test files rather than with a live cluster of machines and real network timing.
The repository layout shows how much of the project is test infrastructure. There is a `raft_paper_test.go`, which is the natural place for tests that follow the paper, a `diff_test.go`, an `interaction_test.go`, snapshot tests in `raft_snap_test.go`, and flow control tests in `raft_flow_control_test.go`. There is a `rafttest/` directory for a test harness, a `testdata/` directory holding the data driven cases, and a `tla/` directory, which is where the TLA+ specifications live. The `tracker/` and `quorum/` directories hold the vote counting that both the leader and the followers need, and `confchange/` handles membership changes, which are the fiddliest part of Raft to get right.
The build file backs this up. The `Makefile` has a `verify` target that chains `verify-gofmt`, `verify-dep`, `verify-lint`, `verify-mod-tidy` and `verify-genproto`, plus a `test` target that runs the unit passes through `scripts/test.sh`.
Starting a node, and the three ways a cluster comes into being
The primary object in raft is a `Node`. You either start one from scratch with `raft.StartNode` or from some initial state with `raft.RestartNode`.
A three node cluster looks like this:
storage := raft.NewMemoryStorage()
c := &raft.Config{
ID: 0x01,
ElectionTick: 10,
HeartbeatTick: 1,
Storage: storage,
MaxSizePerMsg: 4096,
MaxInflightMsgs: 256,
}
// Set peer list to all nodes in the cluster.
// Note that other nodes in the cluster, apart from this node, need to be started separately.
n := raft.StartNode(c, []raft.Peer{{ID: 0x01}, {ID: 0x02}, {ID: 0x03}})A single node cluster is the same shape with the peer list containing only itself, so that the node can become the leader of its own cluster.
The third case is how a node joins an existing cluster, and it is the one that surprises people. To allow a new node to join, you do not pass in any peers. You first add the node to the cluster by calling `ProposeConfChange` on any existing node inside the cluster, and then start the new node with an empty peer list:
// Create storage and config as shown above.
n := raft.StartNode(c, nil)Restarting a node from previous state is a separate entry point, and the storage has to be reconstructed before the node is created:
storage := raft.NewMemoryStorage()
// Recover the in-memory storage from persistent snapshot, state and entries.
storage.ApplySnapshot(snapshot)
storage.SetHardState(state)
storage.Append(entries)and then `n := raft.RestartNode(c)`, with no peer information, because the peer information is already in the storage.
Note that `NewMemoryStorage` is what the examples use. It is fine for a demo and useless in production, which is the point of the library leaving storage to you.
The Ready contract is where a cluster breaks if you get it wrong
After creating a Node, the user has responsibilities, and the documentation is specific about them. The first is to read from the Node.Ready() channel and process the updates it contains.
Step one is persistence order. You must write Entries, HardState and Snapshot to persistent storage in order, Entries first, then HardState and Snapshot if they are not empty. If the storage supports atomic writes, all of them can be written together. And when writing an Entry with index i, any previously persisted entries with index >= i must be discarded, which is the rule that makes a truncated tail of the log safe.
Step two is message sending, and it has one hard constraint and one permitted optimisation. The constraint is that no messages may be sent until the latest HardState has been persisted to disk, and all Entries written by any previous Ready batch. Messages may be sent while entries from the same batch are still being persisted. The optimisation is to let the leader write to disk in parallel with its followers, as described in section 10.2.1 of the Raft thesis, to reduce I/O latency. And if a message has type `MsgSnap`, the user calls `Node.ReportSnapshot()` after it has been sent, because those messages may be large.
The persistence rule is the one to get right. Sending a vote or an append before the corresponding log entry is durable is how a cluster loses data, and the library cannot detect it for you, because the library does not own the disk.
Two read paths, and only one of them trusts the clock
Read-only queries are where Raft implementations differ most, and etcd-io/raft offers two mechanisms for linearizable reads served by both the leader and the followers.
The first is the quorum-checked read. The leader checks with quorum and bypasses the Raft log before processing read-only queries. A follower asks the leader to get a safe read index before processing read-only queries. This is correct without assuming anything about clocks, at the cost of a round trip to the leader for every follower read.
The second is the lease-based read, and the documentation describes it as more efficient. The leader bypasses the Raft log and processes read-only queries locally, and followers still ask the leader to get a safe read index. The important caveat is in the last line of that description: this approach relies on the clock of all the machines in the Raft group.
That word is the whole story. A lease-based read is a performance optimisation bought with a synchrony assumption, and clock skew between nodes turns that assumption into a correctness question. Whether you can take that trade depends on your deployment, and if your machines run in a cloud with aggressive clock drift or leap second handling, it is a decision to make deliberately.
The rest of the feature list covers the rest of the protocol. Leader election, log replication, log compaction, membership changes, and a leadership transfer extension. The optional enhancements are where the engineering shows: optimistic pipelining to reduce log replication latency, flow control for log replication, batching Raft messages to reduce synchronised network I/O calls, batching log entries to reduce disk synchronised I/O, writing to the leader's disk in parallel, internal proposal redirection from followers to the leader, automatic stepping down when the leader loses quorum, and protection against unbounded log growth when quorum is lost.
Where etcd-io/raft is the wrong tool
The boundary is the library's design, and it is not negotiable through configuration.
First, this is not a database, a queue, or a cluster manager. There is no client, no server, no storage engine, no snapshot transfer over the wire, and no membership discovery. The documentation's own answer for a team that wants a replicated key-value store is to use etcd. What this repository gives you is the agreement layer, and the expectation that you will write the other three quarters of a distributed system yourself.
Second, you must implement the transport, and the transport is where consensus bugs hide. The library tells you to send messages to the nodes named in the `To` field, but it does not tell you how to serialise them, whether to retry, or how to handle a peer that has gone away. A library that refuses to own the wire is also a library that cannot protect you from your own retry logic.
Third, the Go version floor. The module is `go.etcd.io/raft/v3` and the manifest declares `go 1.26` with `toolchain go1.26.9`. That is a high floor for a library, and it is a real constraint for organisations still on older Go releases.
Fourth, the read paths deserve a second look, as described above, since the lease-based variant trades correctness for speed under a clock assumption.
Finally, the maturity claim needs a date attached. The documentation says that as of 2016 this is the most widely used Raft library in production, serving tens of thousands of clusters each day. That is a statement from 2016. The release history is much thinner than the user list suggests, with v3.6.0 on 2025-02-05 and v3.7.0 on 2026-06-23, and the last push to the repository on 2026-10-19.
Apache-2.0 terms and a codebase that documents its own design
The licence is Apache-2.0, with the licence file at the repository root, which is the expected choice for a library that many projects embed in server software, and it carries the usual patent grant and NOTICE obligations.
The repository is unusually well organised for something this small. `node.go` and `rawnode.go` hold the two API surfaces, with `rawnode.go` giving the lower level access that bypasses the `Ready` loop. `log.go` and `log_unstable.go` separate the committed log from the not yet committed tail, which is the piece that makes snapshotting and log compaction correct. `read_only.go` implements the read-index machinery behind both read paths, `status.go` exposes node status, `state_trace.go` and `state_trace_nop.go` provide tracing with a build-tag alternative, and `raftpb/` holds the protocol buffer definitions generated by the `verify-genproto` target.
The `design.md` file at the root and the `tla/` directory together are the real documentation for anyone changing the algorithm. The TLA+ specifications let you state the invariants and check them, which is the level of rigour you would expect from a library whose users run it in the control plane of a Kubernetes cluster.
The dependency list in `go.mod` is short: the CockroachDB datadriven test framework, testify, and the protobuf runtime. The library itself pulls in nothing beyond the standard library and protobuf, which is another way of saying the minimalism is real rather than marketing.
Editorial conclusion
etcd-io/raft fits a team building a replicated state machine that already has, or wants to own, its network layer and its durable log, and that values a small deterministic core it can test with data driven and TLA+ based tests. It does not fit a team looking for a database, a queue, or a cluster to run, since the library ships none of those, and it is the wrong choice if you are not prepared to implement persistence and message transport yourself. Before you build on it, confirm your Go toolchain matches the `go 1.26` directive in `go.mod`, and read the Ready contract carefully, because persisting entries out of order is the mistake that breaks a cluster.
Frequently asked questions
Does etcd-io/raft include storage and networking?
No. The library implements only the Raft algorithm, and the documentation states that both network and disk IO are left to the user. You must implement your own transport for messages between Raft peers and your own storage layer to persist the Raft log and state.
How do I start a three node Raft cluster in Go?
Create a `raft.Config` with an ID, ElectionTick, HeartbeatTick, a Storage, MaxSizePerMsg and MaxInflightMsgs, then call `raft.StartNode(c, []raft.Peer{{ID: 0x01}, {ID: 0x02}, {ID: 0x03}})`. The other nodes have to be started separately.
What is the order in which I must persist Ready updates?
Write Entries, HardState and Snapshot to persistent storage in order, Entries first, then HardState and Snapshot if they are not empty. When writing an Entry with index i, any previously persisted entries with index >= i must be discarded, and no messages may be sent until the latest HardState is on disk.
What is the difference between the two linearizable read paths in etcd-io/raft?
The quorum-checked read has the leader check with quorum and bypass the Raft log, while followers ask the leader for a safe read index. The lease-based read has the leader bypass the Raft log and serve locally, and the documentation states that this approach relies on the clock of all the machines in the Raft group.
What Go version does etcd-io/raft require?
The module is `go.etcd.io/raft/v3` and the manifest declares `go 1.26` with the `toolchain go1.26.9`. The direct dependencies are the datadriven test framework, testify and the protobuf runtime.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/etcd-io-raft)Was this useful?