Consistent Hashing Explained Visually
A 4-node session cache scales to 5 nodes to handle more traffic. With hash(key) % N, that single node addition changes almost every key's target node, and the cache that was supposed to absorb load instead misses on nearly everything at once. The fix is not a bigger cluster — it is a different mapping from key to node.
This guide follows session keys user-1001 through user-1004 across a cache cluster. We will see exactly why modular hashing remaps almost everything on resize, place both keys and nodes on a hash ring so only a fraction of keys move, add virtual nodes to fix the ring's uneven load, trace a real scale-out that stayed lopsided anyway, and decide when the extra mechanism is worth building.
Modular hashing breaks when the node count changes
hash(key) % N is simple and, for a fixed N, perfectly uniform if the hash function is uniform. The problem is what happens when N changes. Because the modulus itself changes, the target node for almost every key changes too — not just the keys that logically belong to the new or removed node.
For a cache, that means a scale-out event — the moment meant to relieve load — instead invalidates nearly the entire cache at once. Every client's next read misses, every miss falls through to the origin database, and the database that was already under pressure now takes the full read traffic it was being protected from. This is the mechanism behind cascading failures during "routine" capacity changes.
Consistent hashing exists to fix exactly this one property: when the node count changes by one, only the keys that node was responsible for should move. Everything else should stay put.
Quick reference
- hash % N is uniform for a fixed N, not stable across a changing N.
- Adding or removing one node moves roughly (N-1)/N of all keys.
- A cache scale-out under modular hashing can trigger a near-total cache miss storm.
- The miss storm hits the origin store precisely when it was already under load.
- Goal: a node-count change should move only that node's share of keys.
Remember this
Modular hashing is uniform for one fixed cluster size but remaps nearly every key the moment that size changes — the opposite of what a live cache needs.
The hash ring: put keys and nodes on the same circle
Consistent hashing maps both nodes and keys onto the same circular hash space — typically 0 to 2^32 − 1, wrapping back to 0. Each node gets a position on the ring by hashing its identity (cache-a, cache-b, …). Each key is hashed the same way, and it belongs to the first node found walking clockwise from the key's position.
Adding a node inserts one new point on the ring. Only the keys between that new point and the previous node clockwise — the arc it now owns — move to it; every other key's nearest clockwise node is unchanged. Removing a node deletes its point, and only the keys in its arc fall through to the next node clockwise. In both cases, the fraction of keys that move is close to 1/N, not (N-1)/N.
This is the entire mechanism: a sorted ring of node positions, a query that finds the first node at or after a key's hash, and the property that only a local arc is affected by any single node change.
Quick reference
- Nodes and keys share one hash space; a key belongs to the first node clockwise.
- A ring lookup is a sorted-array search, not a full recomputation over all nodes.
- Adding/removing one node affects only that node's arc — about 1/N of keys.
- The ring itself does not require coordination on every request, only on membership change.
- Hash function quality matters: a skewed hash produces an uneven ring even before load.
Remember this
Placing keys and nodes on one ring turns a node-count change from 'remap nearly everything' into 'remap one arc.'
Virtual nodes: smoothing an uneven ring
One point per physical node sounds fair, but random hash positions are not evenly spaced — with only 4-5 points on a 32-bit circle, gaps vary widely by chance, so one node can end up owning a much larger arc than another purely by the luck of its hash. Worse, when that node fails, its entire arc falls to exactly one neighbor, which now absorbs a large, sudden spike instead of a spread-out one.
Virtual nodes fix both problems: each physical node is hashed to many points on the ring (commonly 100-200), not one. Key ownership is still "first point clockwise," but now each physical node's total share is the sum of many small, scattered arcs, which averages out close to 1/N of the ring regardless of hash luck on any single point. When a node fails, its many small arcs fall to many different neighbors instead of dumping everything on one.
More virtual nodes reduce load variance but increase ring size and lookup/membership bookkeeping — there is a real trade-off, not a free upgrade. Systems like Amazon Dynamo and Apache Cassandra default to somewhere in the 100-256 vnodes-per-node range because it drives per-node load variance down to a few percent without an unreasonable ring size.
Quick reference
- Few points per node → high variance in each node's arc size, purely from hash luck.
- Virtual nodes replace one big arc per node with many small, scattered arcs.
- Failure spreads across many neighbors instead of overloading exactly one.
- More vnodes reduce load variance but grow ring size and membership-update cost.
- 100-200 vnodes/node is a common starting point; tune from measured load variance.
Remember this
Virtual nodes trade a bigger ring for a much fairer one — each physical node's load becomes an average over many small arcs instead of one unpredictable one.
Failure story: still lopsided after scaling out
Trigger. The cache cluster scales from 4 nodes to 5 to relieve cache-b, which has been running hot. The new node cache-e is added to the ring with only 1 virtual node, matching the (already too low) setting the other 4 nodes were configured with. Symptom: minutes after the scale-out, cache-b is still evicting aggressively and its miss rate has barely moved, while cache-e sits nearly idle.
Root mechanism. With one point per node, cache-e's single ring position happened to land inside an arc far from cache-b's share — it stole keys from cache-d instead. cache-b's arc was never touched, because consistent hashing only ever moves keys in the new node's arc, and that arc's location is entirely up to where its one hash point happens to fall. Low vnode count means high variance in exactly where relief lands, and there is no guarantee it lands where the hot spot is.
Evidence. A histogram of keys owned per node shows cache-b at 38% of total keys against an expected ~20%, and cache-e at 3%. Recovery: raise every node's virtual-node count (e.g., to 150) and rebuild the ring — this redistributes existing arcs immediately, without waiting for further scale events. Prevention: treat vnode count as a capacity-planning parameter with a measured variance target, monitor per-node key-count distribution continuously, and alert on skew — not just on absolute node memory.
Quick reference
- Trigger: a new node joins with too few virtual nodes to guarantee balanced relief.
- Symptom: the previously hot node stays hot; the new node stays nearly idle.
- Mechanism: a node change only remaps its own arc — location of that arc is luck with 1 vnode.
- Evidence: per-node key-count histogram, not just per-node memory/CPU alone.
- Prevention: size vnode count for a measured load-variance target; monitor skew continuously.
Remember this
Consistent hashing guarantees a node change only moves that node's arc — it does not guarantee that arc lands where the imbalance actually is, unless vnode count is high enough to average it out.
When consistent hashing pays for itself
Reach for consistent hashing when node membership changes at runtime and each remap has a real cost: distributed caches, sharded key-value stores, and request routers that need session affinity across a fleet that autoscales. The benefit is proportional to how often membership changes and how expensive a full remap is — a cluster that never resizes gets little from the extra mechanism.
If the cluster size is fixed and known ahead of time, plain modular hashing is simpler, faster to reason about, and has zero ring-maintenance cost — do not add consistent hashing for a problem that does not exist yet. If uneven load is a concern independent of resizing (a few very popular keys, not node count changes), consistent hashing alone will not fix it — that needs key-level mitigation such as splitting hot keys or adding a request-level cache in front of the ring.
For durability on top of placement, pair the ring with replication: store each key on the next R-1 distinct physical nodes walking clockwise from its primary, not just the first. That is what gives a ring fault tolerance, not just balanced placement — placement and replication are two different guarantees layered on the same ring.
Quick reference
- Use it when node membership changes at runtime and remap cost is real.
- Skip it for a fixed-size cluster — plain modular hashing is simpler and sufficient.
- It does not fix hot-key skew — that needs key-level mitigation, not more ring points.
- Replicate to the next R-1 distinct physical nodes clockwise for fault tolerance.
- Tune and monitor virtual-node count from measured per-node load variance, not a guess.
Remember this
Consistent hashing earns its complexity when membership changes at runtime and a full remap is expensive — pair it with replication and enough virtual nodes to actually balance load, not just avoid remap storms.
Key takeaway
Modular hashing is simple and uniform until the node count changes, at which point it remaps almost everything at once. A hash ring fixes that by making a node change touch only that node's arc. Virtual nodes fix the ring's remaining problem — arc size varying by hash luck — by spreading each physical node across many small arcs so both normal load and failure redistribute fairly. None of this replaces replication for durability or hot-key mitigation for skew; it solves one specific problem: keeping a remap local when membership changes.
Practice (30 minutes): implement the ring lookup and buildRing functions above with 5 fake nodes and 500 random keys. Record each node's key count with 1 vnode per node, then again with 150. Remove one node and measure what fraction of keys actually moved in each version. You pass when you can show the vnode version keeps per-node share within a few percent and moves close to 1/N of keys on a single node removal — not (N-1)/N.
Related Articles
Explore this topic