Nine hundred keys, five nodes, and two rules for deciding which node holds which key. hash mod n takes the key’s hash modulo the number of nodes. hash ring scatters every node’s name across a 32-bit circle as a few hundred points, and a key belongs to the first point clockwise from where its own hash lands.
Both are drawn over the same key set, so the comparison is honest. Then add a node.
The two strips under “keys that changed node” are the piece. One cell per key, lit if that key changed hands. The ring’s strip flickers — around 1/n of the circle belonged to spans the new node’s points just took over, and nothing else was touched. The modulo strip goes almost solid, because the divisor changed and every key’s arithmetic changed with it. Roughly 12% against 84%, which is the difference between a rebalance and a full cache eviction.
Two more things fall out that the one-line summary never mentions:
Balance is what the ring pays. Drag points/node down to 1 and the circle becomes five fat wedges of wildly unequal size — one node holding 39 keys while another holds 265. Virtual points exist to fix exactly this, and the “worst vs even share” readout shows the lumpiness draining away as you drag back up: ~80% at one point per node, single digits by 240. Modulo never has this problem, and never has the other one either. That is the actual trade.
The ring depends on the node set; modulo depends on the node order.
Remove a node and add it straight back — − node picks a random one, so the
re-added name usually lands at a different index in the node list. The ring
returns to a placement it has held before and says so; modulo moves another
78% of the keys getting somewhere it has never been. Same membership, same
count, different arrangement, and only one of the two rules cares.
The running totals at the bottom are where it stops being a debate. Thirteen node changes in, the ring has relocated ~3,100 keys and modulo ~8,100 — and those keys are cache entries, shard contents, session state, whatever the placement was actually deciding.
Reuse
src/ring.js is a framework-free ES module, no dependencies:
hash32(str)— FNV-1a with murmur3’sfmix32finalizer.makeKeys(count, seed)— a deterministic key set; same arguments, same hashes, so a run is reproducible.HashRing(vnodes)—add,remove,setVnodes,owner(h)(binary search over the sorted points, wrapping at the top),assign(keys), andarcs(), which returns the spans in ring order as{ start, span, node }so a renderer can turn each one straight into an angle.assignModulo(keys, nodes)— the comparison baseline.moved(before, after)—{ count, flags }, whereflagsis aUint8Arrayparallel to the key set. That’s what the strips draw.loads,imbalance,fingerprint— per-node counts, the worst node’s deviation from an even share, and a cheap hash of a whole assignment for spotting a placement you have held before.
No rendering and no timers in the module; the canvas demo is reference code.
Gotchas
- The hash finalizer is load-bearing. Plain FNV-1a over keys that differ
only in trailing characters (
key-1,key-2, …) leaves the outputs in structured clumps, and a ring reads clumped hashes as a lumpy space. The first version of this measured an 80% imbalance at 120 points per node — an artifact of the hash, not of the placement rule.fmix32fixed it. - Modulo is not perfectly balanced either. It is balanced exactly as far as the hash is uniform, which at 900 keys means ±10% of sampling noise. The ring at 240 points per node sits in the same band. The honest claim is that the ring’s balance converges on modulo’s, not that it beats it.
- Changing points/node is a real reconfiguration and is counted as one. It moves keys on the ring and moves nothing under modulo, so that one change scores against the ring — and can leave the running total temporarily in modulo’s favor if you sweep the slider. That is the honest accounting, not a bug; the per-change history plot shows which bars came from what.
− noderemoves a random node rather than the newest, deliberately. LIFO removal would put a re-added node back at its old index and modulo would appear to restore too, which would hide the set-versus-order distinction the round trip exists to show.- The demo bundles its own copy of
ring.js(self-contained by contract); re-copy after editingsrc/.
