Hashes inside data structures
How hashing fills buckets, what load factor does to collisions, and why consistent and rendezvous hashing stop a cluster from reshuffling every key when a node leaves.
Buckets, collisions and load factor
A hash table turns the hash into a bucket index with a modulo or a bitmask. Two keys can land in the same bucket, so the table must resolve collisions, and the load factor decides how often that happens.
# chaining: each bucket holds a list
class HashMap:
def __init__(self, capacity=8, load_factor=0.75):
self.buckets = [[] for _ in range(capacity)]
self.size = 0
self.load_factor = load_factor
def _index(self, key):
# bitmask works only because capacity is a power of two
return hash(key) & (len(self.buckets) - 1)
def put(self, key, value):
if (self.size + 1) / len(self.buckets) > self.load_factor:
self._resize()
bucket = self.buckets[self._index(key)]
for i, (k, _) in enumerate(bucket):
if k == key:
bucket[i] = (key, value)
return
bucket.append((key, value))
self.size += 1
def _resize(self):
old = [pair for bucket in self.buckets for pair in bucket]
self.buckets = [[] for _ in range(len(self.buckets) * 2)]
self.size = 0
for k, v in old:
self.put(k, v)| Load factor | Expected probe length | Behavior |
|---|---|---|
| 0.25 | About 1.2 | Very fast, wastes memory |
| 0.5 | About 1.5 | A good balance for open addressing |
| 0.75 | About 2.5 | The common default for chaining |
| 0.9 | About 5.5 | Degrades sharply; clusters form |
| 1.0 and above | Unbounded | Must resize; every probe is a collision |
Consistent hashing
Modulo arithmetic is fine for one table but catastrophic for a cluster. With hash(key) % N, changing N remaps almost every key, which invalidates every cache entry at once.
import bisect, hashlib
class Ring:
"""Consistent hash ring with virtual nodes."""
def __init__(self, nodes, vnodes=100):
self.points = []
self.owner = {}
for node in nodes:
for i in range(vnodes):
h = int(hashlib.sha256(f"{node}#{i}".encode()).hexdigest()[:8], 16)
self.points.append(h)
self.owner[h] = node
self.points.sort()
def node_for(self, key):
h = int(hashlib.sha256(key.encode()).hexdigest()[:8], 16)
i = bisect.bisect_left(self.points, h) % len(self.points)
return self.owner[self.points[i]]
ring = Ring(["cache-a", "cache-b", "cache-c"])
for k in ("user:1", "user:2", "user:3"):
print(k, ring.node_for(k))- Removing one node from a ring of N moves roughly 1/N of keys, instead of all of them.
- Virtual nodes even out the load: without them, a node may own a huge or tiny arc by chance.
- The ring is a routing decision, not a security control; the hash only needs to be well distributed.
- Rendezvous hashing solves the same problem without a ring: score every node per key and take the maximum.
# rendezvous hashing: no ring, no virtual nodes
import hashlib
def pick(key, nodes):
return max(nodes, key=lambda n: int(
hashlib.sha256(f"{key}|{n}".encode()).hexdigest()[:16], 16))
nodes = ["cache-a", "cache-b", "cache-c"]
print(pick("user:1", nodes))Stability across languages and runs
| Concern | Problem | Solution |
|---|---|---|
| Randomised string hash | Python and Rust randomise per process | Use a stable hash such as xxHash or SipHash with a fixed seed |
| Iteration order | Hash tables reorder on resize | Sort explicitly if order is observable |
| Cross-language tables | Built-in hashes are not portable | Define the hash in the protocol, not the language |
| Serialising a hash map | The byte output differs per run | Serialise a sorted list of pairs instead |
| Sharding by hash | Changing the algorithm reshuffles data | Version the shard scheme and migrate in a controlled way |
💡
A hash value is not a stable identifier unless the algorithm, the seed and the input encoding are all fixed. Two processes, two languages or two versions of a runtime can compute different values for the same key, which is why cross-service sharding needs an explicitly defined hash.
FAQ
Chaining or open addressing?
Chaining is simpler and degrades gracefully; open addressing has better cache behaviour and no per-entry allocation but needs a lower load factor. Most standard libraries pick one per language and hide the choice.
Why does removing one node disturb fewer keys with consistent hashing?
Because each key maps to a point on a ring and its node is the next one clockwise. Removing a node only reassigns the keys in that node's arc.
Related
Cache keys, ETags and shard keys Non-cryptographic hashes: CRC32, FNV and xxHash
Last refreshed 2026-09-18.