Beyond Code & Karma · System Design
Spread keys across a changing set of servers and move as few as possible when one joins or leaves. The hash ring behind every distributed cache and sharded database — explained through clockwise ownership, virtual nodes, and the Kaal Chakra, the wheel that turns without reshuffling the world.
Chapter 01 — The Foundation
Consistent hashing spreads keys — cache entries, database rows, sessions — across a set of nodes so that adding or removing a node moves as few keys as possible. Instead of mapping a key to a node by arithmetic on the node count, you hash both keys and nodes onto the same circular space — the ring — and give each key to the first node you meet travelling clockwise. The node count is no longer in the formula, so changing it barely disturbs anything.
The Cosmic Connection
The Kaal Chakra — the great wheel of time — turns eternally, and yet what is in its right place is not flung aside each turn. The ring of consistent hashing is this same mandala: a circle of duty where each node is the guardian of one arc, and every key finds its rightful keeper by walking clockwise. When a guardian departs — anitya, impermanence — only its arc is handed to the neighbour at hand. The wheel turns; the world is not remade.
"The wheel turns, and the disturbance stays local — that is dharma, and it is also good distributed-systems design."
Chapter 02 — Why Modulo Breaks
The obvious scheme is node = hash(key) % N. Perfectly
balanced, trivially simple — until N changes. Because N is the
modulus, adding or removing one node changes the result for almost
every key: on average (N-1)/N of all keys move. With
10 nodes, growing to 11 re-homes about 90% of the keyspace at once — a
cache-miss stampede onto your database, or a network-saturating
reshuffle of a sharded store.
Consistent hashing breaks the dependency on N. First, hash anything — a key or a server label — to a position on the circle:
// Map any string onto the ring — a point in the space [0, 2³²). function hashToRing(str) { let h = 2166136261 >>> 0; // FNV-1a, 32-bit seed for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; // the key's position on the circle }
hashToRing. A key's position is fixed by the key alone, so
it doesn't shift just because the node set changed. That single fact is
the whole advantage over % N.
| Event | hash % N | Consistent hashing |
|---|---|---|
| Add 1 node (N→N+1) | ~(N-1)/N of keys move | ~1/(N+1) of keys move (one arc) |
| Remove 1 node | ~(N-1)/N of keys move | ~1/N of keys move (that node's arc) |
| Lookup cost | O(1) arithmetic | O(log V) binary search over V virtual nodes |
| Load balance | Perfectly even | Even, given enough virtual nodes |
Chapter 03 — Clockwise Ownership
Hash the key to a point, then sweep clockwise to the first node you hit. Wrap past the top of the circle back to the start. That node is the owner — and it's the same answer on every machine, with no shared state to consult:
// nodes = virtual-node positions, sorted ascending around the ring. // A key is owned by the FIRST node clockwise from its position. function ownerOf(key, nodes) { const p = hashToRing(key); for (const node of nodes) { if (node.pos >= p) return node.server; // first past the key } return nodes[0].server; // walked off the top → wrap to the start } // In production this is a binary search — O(log V) over V virtual nodes.
Hash each server — and each of its virtual replicas — to positions on the ring. Sort them once; this is the routing table.
Each key hashes to its own point and walks clockwise to the first node. No coordinator, no lookup table — just the hash and a binary search.
The topology changes at just one or a few points on the circle. A new node slips between two neighbours; a leaving node vanishes from the sorted list.
The affected arc re-homes to its clockwise neighbour. Every key outside that arc still resolves to exactly the same node it did before.
Chapter 04 — Live Visualization
A small ring, auto-playing. It adds a fourth node, then removes it, on a loop. Only the dots whose owner changed light up — count how few move each time.
Chapter 05 — Deep Dive
This is where the surface-level explainers stop. Here's what actually matters when you run this in production.
With one position per server, the ring is lumpy. By luck, one server can land owning a giant arc and take far more than its share while another starves. The fix is to place each physical server on the ring many times — 100 to 200 replicas at independently hashed positions. Each server now owns many small arcs scattered around the wheel, and the law of averages flattens the load. As a bonus, when a server leaves, its arcs are inherited by many different neighbours instead of dumping the whole load on one.
Three ways to map keys to a changing node set — the cheat-sheet:
| Scheme | Keys moved on change | Trade-off |
|---|---|---|
| hash % N | ~(N-1)/N — almost all | Dead simple, O(1), but catastrophic churn |
| Consistent hashing | ~1/N — one arc | Needs virtual nodes for balance; O(log V) lookup |
| Rendezvous (HRW) | ~1/N — minimal | No ring to maintain; O(N) score per key unless tree-optimised |
Rule of thumb: consistent hashing dominates when the node set changes often and you want cheap lookups. Rendezvous hashing wins when you want minimal moves without maintaining a sorted ring, and N is small.
Consistent hashing bounds how many keys move and evens out the keyspace — but it cannot fix skew in the keys themselves. If one key (a celebrity user, a viral product) gets a disproportionate share of requests, that single key still lands on one node and that node still gets hot. Real systems pair the ring with hot-key detection, replication of popular keys, or request coalescing.
Under hash % N, a key keeps its node after going to
N+1 only if hash % N === hash % (N+1) — which holds for
roughly 1 in N keys. So about (N-1)/N of keys
change owners. On the ring, a new node only captures the arc
between itself and its predecessor — an expected 1/(N+1)
slice of the circle — so only that fraction re-homes, and to the
new node alone.
It's the default partitioner anywhere keys spread over a changing node set:
| System | What it partitions |
|---|---|
| Memcached clients | Cache keys across nodes, so adding a node doesn't cold-start the whole cache. |
| DynamoDB / Cassandra | Partitions placed on a ring with virtual nodes for even, low-churn distribution. |
| Load balancers | Consistent-hash balancing pins a client to the same backend (sticky sessions) as the pool changes. |
| Object / blob stores | Spread objects across storage nodes while keeping rebalance minimal on scale-up or failure. |
Chapter 06 — Dharma & The Ring
Karma is the principle that change is constant, yet what is in its right place need not be undone. No node owns the whole world; each holds only its arc. When a node joins or departs, its duty passes only to the neighbour at hand — not to all.
// Add a node: it claims only the arc behind it… ring.add('node-D'); // ~1/N of keys re-home to D; the rest never move // …remove a node: its arc passes to the next node clockwise. ring.remove('node-B'); // only B's keys move — the wheel turns on, undisturbed
This is the discipline of the Kaal Chakra: let the wheel turn without reshuffling everything already settled in its rightful seat. Virtual nodes are many hands sharing one load so none is crushed; minimal remapping is order preserved through impermanence. The ring holds no grudge and plays no favourites — it keeps the disturbance local and moves on.
"Do not turn the whole wheel to seat one new guest. Hand on only the arc at hand." — the Gita, interpreted for distributed systems 🙏
Chapter 07 — Test Yourself
Three questions. No cheating. Your karma is watching.
The Enlightenment
कर्म करो, फल की चिंता मत करो