come kill my cluster!

try to kill my cluster! :)

the live cluster

connecting…
users
-
uptime
-
since loss
-
writes/s
-
reads/s
-
vm cpu
-
vm mem
-

grafana

open grafana

time series for leader, term, commit index, writes/s, reads/s, host cpu, and host memory. panel titles use a short gloss in parentheses.

each card is one machine. it shows that machine's role, its term, and how far it has committed.

four of the seven have to stay alive, which makes the leader the interesting kill. machines heal themselves after about ten seconds, and you get one kill every two seconds.

one thing to watch for in the readout: writes and reads per second are the leader's, while vm cpu and vm mem are the whole host.

what this is

kill-my-cluster is a raft key-value store i wrote from scratch in go. there's no consensus library under it, so the log, the elections, the replication, and the read path are all mine. seven of those nodes run in docker on a single oracle vm with 4 ocpu.

it's a demo and a learning project, not a production database. i wouldn't put anything you care about in it.

the problem it mocks

put your data on one machine and you've got two problems. the machine can die and take the data with it. worse, it can stay up and quietly serve wrong answers β€” and from the outside, those two look the same.

the fix: keep the data on several machines, and make them agree on the order of every change before any of them acts on it. that agreement is consensus, and raft is one way to reach it.

you're probably already depending on it. etcd runs raft, and every kubernetes cluster keeps its entire state there. consul and cockroachdb run it too.

this page is that shape at small scale, where the failure is a button instead of a paragraph.

how it works

start with the leader. one node holds that job, and every change goes to it first. the leader writes the change into its log, then ships it to the other six. once four of the seven have it, the change is committed and applied to the map each node keeps in memory.

four is the whole trick. any two majorities have to share at least one node, so two conflicting decisions can never both commit.

one more word you'll need: term. it's the election round number, and it goes up by one each time the cluster picks a new leader.

kill a follower and not much happens. kill the leader and there's a short pause while the rest elect a replacement.

how the nodes talk

there are only three messages in the whole protocol, and the one below does almost all the work. here's a single write making the round trip.

one write, start to finish a write arrives at the leader, which appends it to its own log and flushes it to disk. the leader then sends an appendentries message to the other six containing the new entries, its term, and how far it's committed. each follower checks that the entry before this one matches what it already holds, and if so appends and flushes to disk before replying yes. once four of the seven hold the entry the leader marks it committed, and the next appendentries carries that new commit point so the followers apply it too. a heartbeat is the same message carrying no entries. the leader the other six 1. write it down first append to log, fsync to disk 2. AppendEntries: the new entries, my term, my commit point 3. every follower checks does the entry before it match? then append and fsync 4. yes, it's on my disk 5. that's 4 of the 7, so the write is committed 6. the next AppendEntries carries the new commit point 7. apply it to the map, where reads can finally see it a heartbeat is this same message with no entries in it. that's all a leader does to say i'm still here: about seven of them a second.

the other two messages are requestvote, which is the next diagram, and installsnapshot.

installsnapshot is for a follower that's been gone long enough that the entries it needs were already compacted away. the leader gives up on catching it up entry by entry and ships the whole map instead.

that's what happens to every machine you kill here: ten seconds away at this write rate is further behind than the leader bothers to remember.

what an election looks like

this is the part i got wrong first, and the part i'm most pleased with now.

how the cluster replaces a dead leader while the leader is alive it sends an empty appendentries roughly every 150 milliseconds, and each one resets every follower's election timer. when you kill the leader those heartbeats stop. each follower is waiting a different random amount of time between 750 and 1500 milliseconds, so one of them times out first rather than all of them at once. that node doesn't immediately start an election: it first runs a pre-vote poll asking whether the others would vote for it, without anyone changing their term. only if it wins that poll does it increment the term and hold the real vote, and once four of the seven vote yes it becomes leader and starts heartbeating again. measured on the live cluster, that takes between 2.6 and 2.9 seconds from the moment you click. everything is fine the leader sends an empty AppendEntries about every 150ms. every one of them resets the other six timers, which is the only reason nobody calls an election. you kill the leader the heartbeats stop. nothing dramatic happens yet, because none of the six knows the difference between a dead leader and a slow one. one timer runs out first each node waits a different random 750 to 1500ms, so they almost never wake together. spreading them out is what stops six candidates splitting the vote. it asks before it campaigns pre-vote: would you vote for me, if i asked? nobody changes their term to answer. a node that can't win now disturbs nobody. this is the fix for the storm below. it wins, and the term goes up by one only now does it raise the term and ask for real. four yes votes and it's the leader, heartbeating. measured 2.6 to 2.9 seconds from click to new leader.

the pre-vote step in the middle isn't in the original paper β€” it's from the author's thesis, and skipping it cost me six days.

here's what goes wrong without it. a node that's cut off keeps timing out, and under the naive rule every timeout raises its term. when it comes back, it's holding a term nobody else has seen, so the real leader stands down out of politeness and the cluster elects again.

mine reached term 36,000 doing that on a loop, burning 87% of the cpu to commit nothing at all. asking first costs one round trip and makes the whole failure mode impossible.

how the pieces fit together

the cluster itself is only part of it, so read the diagram below in two halves.

the top half is everything sitting behind the panel above: how a kill you press actually reaches a machine. the bottom half is what that kill does once it lands, which is where raft stops being plumbing and starts being the whole point.

how the pieces fit together your browser reaches a cloudflare tunnel over https, and that tunnel is the only way in. behind it, on one oracle vm, a control plane serves the kill buttons and the live updates while grafana serves the charts. both read from a prometheus that is never exposed publicly. the control plane kills and heals seven raft nodes, prometheus scrapes those nodes for metrics, and a load generator keeps steady writes and reads flowing to them. the lower half shows what a kill does inside those seven nodes. a write is only committed once four of the seven hold it on disk, so killing one follower leaves six alive and writes never pause; killing the leader costs a short election before writes resume; and killing four leaves only three alive, which is not a majority, so writes are refused until the machines heal. you, on this page everything below runs on one oracle vm https, the only way in cloudflare tunnel kills, live updates charts control plane kill and heal api grafana the charts you see above asks for rates queries prometheus private, never public load generator so it's never idle kill and heal scrapes metrics writes and reads seven raft nodes one leader, six followers, replicating over grpc and here's what your kill does inside those seven the rule underneath all three: a write counts only once 4 of the 7 hold it on disk you kill a follower 6 of 7 still alive. that's still a majority, so writes never pause. you kill the leader the six left vote in a new leader, the filled dot moves, then writes resume. you kill four only 3 left, and no dot is filled. no majority means no leader, so writes are refused.

the filled dot is the leader, and the crossed ones are machines you killed.

the third case is the interesting one. raft would rather refuse your write than take it on three machines and lose it, so a cluster with no majority stops instead of guessing. the machines come back on their own about ten seconds later, and the filled dot reappears.

a note on the plumbing: node_exporter runs alongside prometheus and reports the host cpu and memory you see in the readout, and everything except the tunnel is a docker container on the same box. there's no separate demo hostname, so this page only ever sees what the control plane chooses to share.

how i built it

i started at the bottom, with a write-ahead log and snapshots on disk. nothing above that layer means much if a crash halfway through a write can corrupt the record, so it felt like the honest place to begin.

the raft core came next, then a key-value map on top of it with per-client ids, so a retried write applies once instead of twice.

all of that worked, and then i made my first mistake: i sent reads through the raft log the same way as writes.

that's technically correct. it's also thousands of proposals a second squeezed through one leader and six disks. on my laptop it election-stormed until throughput fell apart.

the fix was readindex, from section 6.4 of the paper. the leader confirms it still leads with a single round of heartbeats, waits for its own commits to land, then answers straight from memory. reads stopped touching disk, and the whole thing went quiet again.

so i shipped it. seven containers, a control plane behind the kill buttons, load generators, prometheus, grafana, and a cloudflare tunnel. i ran a load ladder to find where the box actually gave out instead of picking a number that sounded impressive, and put the demo online.

then in july it broke for six days straight, which is the part i'd rather not have learned the hard way.

the raft log was never being compacted, so it grew to 21.6 million entries. every restart replayed all of them, pushing the node past 10gb of memory until the kernel killed it β€” which triggered another election, which restarted another node.

term passed 36,000. the host got so busy that ssh barely answered, and getting back in meant racing a loop against the boot window.

the fixes came from reading what actually happened rather than turning knobs:

the third mistake was smaller and, honestly, my favorite. the table that remembers client ids swept itself clean on every write, so once the load generator ran 96 workers it was scanning the whole thing constantly, on all seven nodes. writes fell from about 950 a second to 11.

raft itself stayed perfectly healthy the entire time β€” term flat, quorum fine β€” which is exactly why it took me so long to find. it looked nothing like a consensus problem because it wasn't one.

three mistakes, two of which took the live cluster down. they taught me more than the parts that worked on the first try.

what it does today

as i write this the cluster is holding about 1,160 writes and 1,500 reads a second, with the host near 73% busy across its 4 ocpu. the readout above is the live version of those numbers, so it won't match exactly, and it dips while people are killing things.

the load generator aims at 1,400 writes and 2,000 reads, but it backs off when errors or latency climb. those are ceilings it moves toward, not rates it holds regardless.

one thing that looks alarming but isn't: followers routinely trail the leader by hundreds of commit indexes under load. that's ordinary catch-up, not drift.

the ladder below is where this box saturates, measured 2026-07-21 before the cpu caps existed. every step held term at 1 with no errors, so what ran out was cpu rather than stability.

step setpoint w/r delivered w/r term cpu idle β‰ˆ busy
c0 smoke 80 / 160 80 / 160 1 flat ~49% ~51%
c1 500 / 2000 474 / 1435 1 flat ~10% ~90%
c2 1000 / 4000 767 / 1786 1 flat ~8% ~92%
c3 1500 / 5000 923 / 1872 1 flat ~13% ~87%
c4 1500 / 6500 972 / 2192 1 flat ~6% ~94%
c5 peak 1750 / 6500 1413 / 3233 1 flat ~10% ~90%
c6 1750 / 7000 1007 / 2139 1 flat ~5% ~95%

c5 was the peak. c6 asked for more and got less, which is what a saturated machine looks like from outside. today's rates sit below that on purpose, so kills still have headroom.

what i'd still fix

learn more

the code's at kill-my-cluster, laid out roughly like this:

if raft's new to you, this is the order i'd go in:

  1. kill the leader in the panel above and watch term and commit index move.
  2. read the secret lives of data, which animates the whole algorithm in a few minutes. it's the friendliest starting point i know of.
  3. watch diego ongaro's talk below. he wrote the paper, and hearing him explain it is easier than reading it cold.
  4. then read the paper itself, sections 5 and 6. section 6.4 is the read path i described above, and raft.github.io lists other implementations if you want to compare.
  5. clone the repo, bring up compose, and read internal/raft/node.go with the paper open beside it.

the nodes talk over grpc with protocol buffers. core concepts and the go quickstart are enough to follow the code.