Consistent hashing is a way of assigning keys to servers so that adding or removing a server moves only a small fraction of the keys. It is what lets distributed caches, key-value stores (Dynamo, Cassandra) and sharded databases grow and shrink without reshuffling all their data.
Seats around a round table with guests (keys) sitting between hosts (servers); each guest is served by the next host clockwise. When a new host sits down, only the guests right before them change hosts. Everyone else keeps their host.
N = 4 servers: server = hash(key) % 4
N = 5 servers: server = hash(key) % 5
For a key to stay put, hash % 4 must equal hash % 5 -> true for only ~1/5
of keys. Adding ONE server moves ~80% of all keys.
For a cache: ~80% misses at once -> stampede on the database.
For a database: ~80% of the data has to be copied between machines.
Hash both servers and keys onto the same circle (0 .. 2^32-1).
A key belongs to the first server clockwise from it.
0
S1 * * k1 -> S2
k4 * * S2
S4 * * k2 -> S3
* k3 -> S4 * S3
Add S5 between S2 and S3: only keys in (S2, S5] move from S3 to S5.
Remove S3: only S3's keys move, to the next server clockwise.
On average only K/N keys move (K keys, N servers).
With a few servers placed at random points, arcs are uneven, so some servers own far more keys than others, and when one leaves, all its load dumps onto a single neighbour. The fix is virtual nodes: each physical server is hashed to many points on the ring (e.g. 100–200). Load evens out statistically, a departing server's keys spread across many servers, and a bigger machine can simply get more virtual nodes.
class Ring:
def __init__(self, vnodes=150):
self.vnodes, self.points, self.owner = vnodes, [], {}
def add(self, server):
for i in range(self.vnodes):
h = hash64(f"{server}#{i}")
bisect.insort(self.points, h); self.owner[h] = server
def remove(self, server):
for i in range(self.vnodes):
h = hash64(f"{server}#{i}")
self.points.remove(h); del self.owner[h]
def lookup(self, key):
i = bisect.bisect_right(self.points, hash64(key)) % len(self.points)
return self.owner[self.points[i]] # O(log V) with binary search
For replication, a key is stored on the first R distinct physical servers clockwise (its "preference list"), which is how Dynamo-style stores place replicas.
You gain cheap scaling and smooth failure handling at the cost of a routing layer (every client or router must know the ring) and some complexity in rebalancing. Load is only statistically even, so monitor per-node load and adjust vnode counts.