Naive hashing (hash(key) % N) works fine until you add or remove a node — then N changes and almost every key maps to a different server. For a distributed cache or shard, that means a near-total cache miss storm or a mass data migration, right when you can least afford it.
Consistent hashing fixes this by mapping both nodes and keys onto the same ring, so a topology change only remaps the keys between the changed node and its neighbor.
The Ring
Each node is hashed onto the ring (often multiple times — see "Virtual Nodes" below). To find which node owns a key, hash the key and walk clockwise until you hit the first node.
Adding or Removing a Node
When node B leaves, only the keys that were mapped to B move — to B's clockwise neighbor. Every other key stays exactly where it was. That's the whole point: roughly 1/N of the keyspace moves per topology change, not the whole keyspace.
Virtual Nodes (Replicas)
Hashing raw node names onto the ring directly creates hot spots — some nodes end up owning much bigger arcs than others. The fix: hash each node multiple times, e.g. node-A#0, node-A#1, ..., node-A#149 (150 virtual nodes is a common default). More virtual nodes means smoother distribution, at the cost of a bigger in-memory ring.
Implementation
class ConsistentHash
{
private array $ring = [];
private array $sortedKeys = [];
private int $replicas;
public function __construct(int $replicas = 150)
{
$this->replicas = $replicas;
}
public function addNode(string $node): void
{
for ($i = 0; $i < $this->replicas; $i++) {
$this->ring[$this->hash("$node#$i")] = $node;
}
$this->sortedKeys = $this->sortedRingKeys();
}
public function getNode(string $key): ?string
{
if (empty($this->sortedKeys)) {
return null;
}
$hash = $this->hash($key);
foreach ($this->sortedKeys as $ringKey) {
if ($ringKey >= $hash) {
return $this->ring[$ringKey];
}
}
return $this->ring[$this->sortedKeys[0]];
}
private function sortedRingKeys(): array
{
$keys = array_keys($this->ring);
sort($keys);
return $keys;
}
private function hash(string $value): int
{
return crc32($value);
}
}import bisect
import zlib
class ConsistentHash:
def __init__(self, replicas: int = 150):
self.replicas = replicas
self.ring: dict[int, str] = {}
self.sorted_keys: list[int] = []
def add_node(self, node: str) -> None:
for i in range(self.replicas):
self.ring[self._hash(f"{node}#{i}")] = node
self.sorted_keys = sorted(self.ring.keys())
def get_node(self, key: str) -> str | None:
if not self.sorted_keys:
return None
h = self._hash(key)
index = bisect.bisect_left(self.sorted_keys, h)
if index == len(self.sorted_keys):
index = 0
return self.ring[self.sorted_keys[index]]
@staticmethod
def _hash(value: str) -> int:
return zlib.crc32(value.encode())Common Pitfalls
Too few virtual nodes leads to uneven load, with hot spots on a handful of physical nodes. Using a weak or poorly-distributed hash function makes the same problem worse — prefer something like CRC32 or xxHash over a naive sum-of-bytes hash. Forgetting to remove all of a node's virtual nodes on removal leaves ghost entries that route traffic to a dead server. And rehashing the entire keyspace on every topology change defeats the whole purpose — only the ring should change, not every key's mapping.
When to Reach for This
Distributed caches doing client-side sharding, CDN edge selection, distributed hash tables, sharded databases, and load balancers that need session affinity without a central coordinator. If your cluster size is fixed and rarely changes, plain modulo hashing is simpler and perfectly fine.
Try the challenge
Practice what you just learned. Write your solution, reveal hints if you get stuck.
Instructions
Implement a ConsistentHash class with addNode(node) and getNode(key). addNode() must hash the node name once per replica (use `replicas` virtual nodes per real node) and place each hash onto the ring. getNode() must hash the key and return the name of the node at the first ring position that is greater than or equal to that hash, wrapping around to the smallest ring position if the hash is past the last one. Fill in the TODOs.
Starter code
class ConsistentHash
{
private array $ring = [];
private array $sortedKeys = [];
private int $replicas;
public function __construct(int $replicas = 150)
{
$this->replicas = $replicas;
}
public function addNode(string $node): void
{
// TODO: hash "$node#$i" for each replica (0..replicas-1) and store it in $this->ring
// TODO: rebuild $this->sortedKeys from the ring
}
public function getNode(string $key): ?string
{
// TODO: return null if the ring is empty
// TODO: hash $key, then find the first ring position >= that hash
// TODO: wrap around to the smallest ring position if none found
}
private function sortedRingKeys(): array
{
// TODO: return the ring's keys, sorted ascending
}
private function hash(string $value): int
{
// TODO: return crc32($value)
}
}
import bisect
import zlib
class ConsistentHash:
def __init__(self, replicas: int = 150):
self.replicas = replicas
self.ring: dict[int, str] = {}
self.sorted_keys: list[int] = []
def add_node(self, node: str) -> None:
# TODO: hash f"{node}#{i}" for each replica (0..replicas-1) and store it in self.ring
# TODO: rebuild self.sorted_keys from the ring
pass
def get_node(self, key: str) -> str | None:
# TODO: return None if sorted_keys is empty
# TODO: hash key, then bisect_left into sorted_keys to find the first position >= that hash
# TODO: wrap around to index 0 if past the end
pass
@staticmethod
def _hash(value: str) -> int:
# TODO: return zlib.crc32(value.encode())
pass
Your solution
Hints
All hints revealedUse crc32/zlib.crc32 truncated to a fixed integer range as the ring position
A sorted array + binary search (bisect in Python) makes getNode() O(log N) instead of scanning the whole ring
class ConsistentHash
{
private array $ring = [];
private array $sortedKeys = [];
private int $replicas;
public function __construct(int $replicas = 150)
{
$this->replicas = $replicas;
}
public function addNode(string $node): void
{
for ($i = 0; $i < $this->replicas; $i++) {
$this->ring[$this->hash("$node#$i")] = $node;
}
$this->sortedKeys = $this->sortedRingKeys();
}
public function getNode(string $key): ?string
{
if (empty($this->sortedKeys)) {
return null;
}
$hash = $this->hash($key);
foreach ($this->sortedKeys as $ringKey) {
if ($ringKey >= $hash) {
return $this->ring[$ringKey];
}
}
return $this->ring[$this->sortedKeys[0]];
}
private function sortedRingKeys(): array
{
$keys = array_keys($this->ring);
sort($keys);
return $keys;
}
private function hash(string $value): int
{
return crc32($value);
}
}
import bisect
import zlib
class ConsistentHash:
def __init__(self, replicas: int = 150):
self.replicas = replicas
self.ring: dict[int, str] = {}
self.sorted_keys: list[int] = []
def add_node(self, node: str) -> None:
for i in range(self.replicas):
self.ring[self._hash(f"{node}#{i}")] = node
self.sorted_keys = sorted(self.ring.keys())
def get_node(self, key: str) -> str | None:
if not self.sorted_keys:
return None
h = self._hash(key)
index = bisect.bisect_left(self.sorted_keys, h)
if index == len(self.sorted_keys):
index = 0
return self.ring[self.sorted_keys[index]]
@staticmethod
def _hash(value: str) -> int:
return zlib.crc32(value.encode())

