Consistent Hashing Cache Shard in Python

A minimal consistent hashing ring with virtual nodes that distributes cache keys across shards and minimizes re-mapping when a node is removed.

Medium Python 3.9+ Aug 9, 2026 Caching & Redis 16 views 0 copies

Python code

48 lines
Python 3.9+
import hashlib
import bisect


class ConsistentHashRing:
    def __init__(self, nodes=None, replicas=3):
        self.replicas = replicas
        self.ring = {}
        self.sorted_keys = []
        if nodes:
            for node in nodes:
                self.add_node(node)

    def _hash(self, key):
        return int(hashlib.md5(key.encode()).hexdigest(), 16)

    def add_node(self, node):
        for i in range(self.replicas):
            key = self._hash(f"{node}:{i}")
            self.ring[key] = node
            bisect.insort(self.sorted_keys, key)

    def remove_node(self, node):
        for i in range(self.replicas):
            key = self._hash(f"{node}:{i}")
            del self.ring[key]
            idx = bisect.bisect_left(self.sorted_keys, key)
            self.sorted_keys.pop(idx)

    def get_node(self, key):
        if not self.sorted_keys:
            return None
        hash_key = self._hash(key)
        idx = bisect.bisect_left(self.sorted_keys, hash_key)
        if idx == len(self.sorted_keys):
            idx = 0
        return self.ring[self.sorted_keys[idx]]


if __name__ == "__main__":
    ring = ConsistentHashRing(["cache-1", "cache-2", "cache-3"])
    data_keys = ["user:42", "order:1001", "product:7", "cart:99", "session:abc"]
    assignments = {k: ring.get_node(k) for k in data_keys}
    print("Initial assignments:", assignments)

    ring.remove_node("cache-2")
    new_assignments = {k: ring.get_node(k) for k in data_keys}
    print("After removing cache-2:", new_assignments)

Output

stdout
Initial assignments: {'user:42': 'cache-3', 'order:1001': 'cache-1', 'product:7': 'cache-2', 'cart:99': 'cache-2', 'session:abc': 'cache-1'}
After removing cache-2: {'user:42': 'cache-3', 'order:1001': 'cache-1', 'product:7': 'cache-1', 'cart:99': 'cache-1', 'session:abc': 'cache-1'}

How it works

The ring maps hash values to nodes using MD5 to generate a 128-bit integer. Each node gets replicas virtual positions (node:0, node:1, etc.) to spread keys evenly. bisect_left finds the first key at or after the data key's hash, wrapping to the start of the ring when reaching the end. Removing a node deletes all its virtual points, and only keys whose hash lands on those points move to the next node — most keys stay put. Add remove_node is O(k log n) where k is the replica count, so the ring stays cheap to update even with hundreds of nodes.

Common mistakes

  • Using MD5 in security-sensitive contexts (it's fine for sharding but not cryptography)
  • Forgetting to wrap around the ring when bisect returns len(keys)
  • Using too few replicas (e.g., 1) causing unbalanced distributions

Variations

  1. Use sha256 instead of md5 by swapping the hash function
  2. Return a sorted list of nodes with the ring for weighted consistency hashing

Real-world use cases

  • Sharding Redis or Memcached keys across a pool of cache instances so each key always lands on the same node.
  • Distributing user sessions across app server instances in a load balancer without a central session store.
  • Mapping file chunks or database rows to partition nodes in a distributed storage system while tolerating node additions.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Caching & Redis

Related tutorials and quizzes for this topic.