v0.2.2Raft written from scratch in Go, 71 tests passing
Raftra
A fault-tolerant key-value store.
We kill its leader every ten minutes. Your writes survive.
Three servers in Mumbai agree on every write using Raft, a consensus algorithm implemented here by hand in Go. Lose any one of them, leader included, and nothing that was acknowledged is lost. The bars next to the name are those three servers, live.
curl -fsSL https://raw.githubusercontent.com/shantanu-1607/Raftra/main/site/install.sh | sh
The recorder
Every two seconds this page asks each server who it thinks leads. Three minutes of answers scroll right to left. Orange is the leader, bone is a follower, red is a dead node. Hover or tap the tape to read any moment.
What happened
Now break one yourself
Five simulated servers running textbook Raft in your browser, slowed down ten times so you can follow every message. The log shows simulated time. Click a server to crash it, click again to bring it back.
Waiting for the first election.
- Vote request
- Append entries or heartbeat
- Reply
- Committed entry
- Not committed yet
Try this: split the network while the leader sits on the small side, send a write, then heal. The stranded leader accepts the write but can never commit it. The real cluster also runs pre-vote, which this simulator leaves out so elections are easier to see.
Get the CLI
One small binary, no dependencies. It talks to the live cluster out of the box and follows the leader when it moves.
Install from your terminalOne line on every system. Each script picks the right build for your CPU and checks it against checksums.txt before installing.
Terminal
curl -fsSL https://raw.githubusercontent.com/shantanu-1607/Raftra/main/site/install.sh | shInstalls to ~/.local/bin. Apple silicon and Intel. Installing this way also avoids the Gatekeeper warning a browser download gets.
Any shell
curl -fsSL https://raw.githubusercontent.com/shantanu-1607/Raftra/main/site/install.sh | shInstalls to ~/.local/bin and tells you if that folder isn’t on your PATH yet. x86-64 and ARM64.
PowerShell
irm https://raw.githubusercontent.com/shantanu-1607/Raftra/main/site/install.ps1 | iexCommand Prompt
powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/shantanu-1607/Raftra/main/site/install.ps1 | iex"Installs to %LOCALAPPDATA%\Programs\raftra and adds it to your user PATH, so open a new terminal afterwards. x64 and ARM64.
| System | Intel or AMD (x86-64) | ARM (64-bit) |
|---|---|---|
| macOS | Intel Mac.tar.gz | Apple silicon.tar.gz |
| Linux | x86-64.tar.gz | ARM64.tar.gz |
| Windows | x64.zip | ARM64.zip |
macOS, downloaded in a browser: the binary isn’t signed, so Gatekeeper blocks it. Clear the flag with xattr -d com.apple.quarantine raftra-cli, or use the script above, which avoids it.
Windows, downloaded as a .zip: unzip it, open a terminal in that folder and run .\raftra-cli.exe status. SmartScreen may warn about an unknown publisher the first time, because the binary isn’t signed.
Verify by hand: compare against checksums.txt. Older versions are on the releases page.
Try it in thirty seconds
Every command goes to the live cluster. Writes land on the leader wherever it is, and the CLI moves on to the next server if one is mid-crash. Run raftra-cli with no arguments for an interactive shell.
Playground rules
- Keys up to 128 bytes, values up to 1 KiB, 10,000 keys in total.
- 5 writes a second per IP address, with bursts of up to 10.
- Everything is wiped at the top of every hour.
- It’s public. Don’t store anything you wouldn’t post on a wall.
$ raftra-cli status ● node2 leads term 2 ⎿ raftra-n1.duckdns.org follower term 2 commit 4 raftra-n2.duckdns.org leader term 2 commit 4 raftra-n3.duckdns.org follower term 2 commit 4 $ raftra-cli set city mumbai ● city = mumbai ⎿ Committed: a majority of nodes stored it. Followed the leader to raftra-n2.duckdns.org. $ raftra-cli get city ● city = mumbai ⎿ Read from raftra-n1.duckdns.org. Reads on a follower can briefly lag the leader. $ raftra-cli delete city ● Deleted city ⎿ Committed: a majority of nodes stored the delete. Followed the leader to raftra-n2.duckdns.org. $ raftra-cli get city ● city isn’t set
How a write survives
Nothing counts as written until a majority of servers has it on disk. Lose any one machine, including the leader, and every acknowledged write is still there.
- Any server takes the request. Clients can talk to whichever node answers first.
- Followers point to the leader. They reply with a 307 redirect to the leader’s address.
- The leader writes it to its own log and syncs the disk before going further.
- The leader sends it to the followers over gRPC on the private network. Each one stores it and replies.
- A majority has it, so it’s committed. The leader applies it to the key-value map and answers 200. Followers apply it on the next heartbeat.
What it guarantees
- At most one leader per term, and votes survive restarts.
- Every server applies the same writes in the same order, and only after a majority stored them.
- Any single server can die and no acknowledged write is lost.
- A server on the losing side of a network split can’t commit anything.
- Logs that disagree are repaired from the leader’s copy.
- Pre-vote stops a returning server from disrupting a healthy leader.
- A new leader commits an empty entry at once, so earlier writes become visible without waiting for traffic.
What it doesn’t do (yet)
- Reads on followers can be stale. There’s no ReadIndex or lease read.
- No snapshots, so the log grows until the hourly wipe.
- Cluster membership is fixed at three nodes.
- No batching: every write is its own disk sync, which is why durable throughput is low.
- No TLS or auth between nodes. The Raft port is only reachable on the private network.
Measured, including the slow parts
Three nodes on one Apple M2 laptop (macOS, Go 1.26.4), 100 concurrent clients, 80% writes. Full logs are in benchmark_results.md.
Throughput
Operations per second through the HTTP API.
Why the gap: each write is its own synced bbolt transaction, taken while the node holds its lock, and on macOS a sync is a full drive-cache flush (F_FULLFSYNC). Group commit would fix it. It isn’t built yet.
Failover
Leader crash to the first write committed by the new leader, 20 trials.
245 ms average
Go microbenchmarks
In-process cluster with real fsync, -count=3.
| Read from the state machine | 29.36 ns |
|---|---|
| Mixed, 80% reads | 2.69 ms |
| One write, quorum commit | 14.83 ms |
| Proposal to quorum commit | 15.58 ms |
Tests
Run with Go’s race detector on every push.
| Unit, persistence and chaos tests | 71 / 71 |
|---|---|
| Chaos scenarios, 10 runs each | 80 / 80 |
| Data races found | 0 |
How it was built
Eight phases from an empty folder to a tested cluster, then a public one.
- FoundationProtobuf and gRPC interfaces, the node skeleton and its event loop.
- Leader electionRandomized timeouts, terms and votes. Exactly one leader per term.
- Log replicationAppendEntries, majority commit and the HTTP gateway for clients.
- PersistenceTerm, vote and log in bbolt, so a crashed node rejoins without losing anything.
- Failure testingAn in-memory network that can cut links and split groups, plus eight fault scenarios.
- Deployment and CLIDocker Compose for three nodes and a CLI that follows leader redirects.
- Benchmarks and metricsA load generator, failover timing and Prometheus metrics.
- Docs and polishREADME, a demo walkthrough and a full test pass.
- Then it went publicAbuse limits, release binaries for six platforms, three AWS servers, the ten-minute kill timer and this page.
Two bugs the public cluster found
A restarted server kept knocking out a healthy leader
When the kill timer’s victim came back, it had been alone long enough to start an election and raise its term. Its first message carried that higher term, and the working leader stepped down for nothing. Every kill caused a second, pointless election.
Fix: pre-vote (Raft thesis §9.6). A server now asks whether it could win before raising its term, and peers that heard from a leader recently say no. In three real-process test runs, the leader was knocked out three times before the fix and zero times after. Rejoining now takes 0.2–0.6 s.
Committed data was invisible after a failover
Raft lets a new leader commit entries from earlier terms only indirectly (§5.4.2). Until somebody wrote something new, the last writes before the crash stayed unapplied on the survivors.
Fix: a no-op entry. Each new leader appends an empty entry in its own term as soon as it wins (§8). Committing it commits everything before it, so the data is back within one heartbeat. Ignoring stale election-timer fires in the same change cut the average failover from 322 ms to 245 ms, and the worst case from 611 ms to 291 ms.