When a backend assigns keys with hash(key) % N, adding or removing one node can send most keys to different destinations. Cache misses surge, databases absorb a sudden load spike, and durable records may need a large migration. A capacity change that looked routine can become an incident. Consistent hashing addresses the routing part of this problem by keeping most key assignments stable when cluster membership changes.
This guide moves from modulo partitioning to hash rings, virtual nodes, fixed slots, and rendezvous hashing. It then places the algorithm inside a production system that also needs replication, topology awareness, membership versions, data transfer, hot-key controls, observability, and rollback. The goal is not to build an entire distributed database from scratch. It is to understand when consistent hashing fits, what it guarantees, and which responsibilities remain outside the hash function.
1. Why modulo partitioning becomes expensive
With three nodes, a simple router can select nodes[hash(key) % 3]. A good hash function may distribute the initial key set evenly. When a fourth node joins, however, the divisor changes from three to four. A key retains its previous owner only when the two calculations happen to select the same position; many other keys move even though their old nodes remain healthy.
before: owner = nodes[hash(key) % 3]
after: owner = nodes[hash(key) % 4]
// A new bucket assignment does not move the stored value.
For a cache, the result is a broad cold-cache event. Concurrent requests bypass the cache and hit the database together, creating a stampede precisely while the cluster is changing. For application-level sharding, the consequence is more serious: the new router looks for a record on the new owner while the bytes still exist on the old owner. The system must migrate data, temporarily read from both places, or introduce another stable mapping layer.
Consistent hashing maps both keys and nodes into a stable hash space. A membership change transfers only the ranges associated with the joining or departing node. Its central benefit is bounded assignment churn. It does not copy bytes, detect failures, resolve conflicting writes, or make replicas consistent.
2. How a hash ring maps a key
Visualize the hash space as a circle that runs from zero to the maximum value and wraps back to zero. Each node is hashed to one or more tokens on that circle. A key is hashed into the same space. A common ownership rule assigns the key to the first token encountered clockwise from the key position.
ring = sort(tokens)
function locate(key):
point = hash(key)
token = first ring token >= point
if token does not exist:
token = ring[0]
return token.owner
A router can use binary search over the sorted token array, making lookup O(log V) where V is the total token count. Adding a token changes ownership only between the preceding token and the new token. Removing it transfers that range to the next token. Unrelated ranges retain their mapping.
Wrap-around is an easy implementation bug. If a key hashes beyond the final token, its owner is the first token, not the last node. Signed-versus-unsigned integer handling, byte order, text encoding, or key normalization can also send one logical key to different nodes in different clients. The hash algorithm, input bytes, comparison rules, and token-selection behavior therefore form a versioned contract.
3. One token per server is rarely balanced enough
If every server receives one random token, ring intervals can differ greatly in size. A node that happens to own a large interval stores more keys and receives more traffic. When it fails, a single neighbor often inherits that large range, producing poor load distribution during the failure.
Virtual nodes, or vnodes, give each physical node many tokens spread around the ring. A machine's share becomes the sum of many small intervals, reducing variance. A new machine receives ranges from multiple existing machines instead of pulling most of its data from one neighbor. Amazon's Dynamo paper describes assigning multiple positions to each node, and the Apache Cassandra architecture documentation explains virtual nodes in its consistent-hashing model.
| Token model | Benefit | Trade-off |
|---|---|---|
| One token per node | Small metadata and a simple mental model | Uneven ranges and concentrated transfers |
| Many virtual nodes | Smoother placement and rebalancing | More metadata, ranges, and transfer streams |
| Fixed logical slots | Operationally clear slot-to-node mapping | Requires explicit slot migration states |
More vnodes are not automatically better. An excessive count enlarges membership metadata, creates many small ranges, complicates repair, and may produce too many concurrent data streams. Select a count from cluster size, acceptable skew, metadata cost, and operational tooling, then test it against representative keys.
4. Weighting heterogeneous nodes
Production fleets are often heterogeneous. New servers may have more memory, one availability zone may have different storage limits, or a canary node should accept only a fraction of normal traffic. Placement must support capacity weights rather than assume all machines are equal. A direct ring technique assigns vnode counts in proportion to relative capacity.
node-a: weight 1.0 -> 128 tokens
node-b: weight 2.0 -> 256 tokens
node-c: weight 0.5 -> 64 tokens
The weight must represent the actual workload bottleneck. Memory capacity may suit a cache, whereas a storage shard may be constrained by IOPS or network throughput. Twice the disk does not imply twice the request capacity when encryption CPU is already saturated. A weight change also causes movement, so a noisy autoscaler should not continuously rewrite placement.
Use thresholds, cooldowns, and a maximum transfer rate. A new node that must warm caches or compact streamed data can ramp its weight in stages. Capacity models should consider stored bytes, request rate, object-size distribution, and operation cost rather than counting keys alone.
5. Rendezvous hashing avoids a ring
Rendezvous hashing, also called highest-random-weight hashing, calculates a deterministic score for each (key, node) pair and selects the node with the highest score. When that node leaves, the candidate with the next-highest score takes ownership. This produces minimal-disruption behavior without a sorted token ring.
function locate(key, nodes):
bestNode = null
bestScore = -infinity
for node in nodes:
score = hash(key + stableNodeId(node))
if score > bestScore:
bestScore = score
bestNode = node
return bestNode
The basic form performs O(N) score calculations per lookup, which can be acceptable for a small node set or a cached routing decision. Its advantages are a compact implementation, a natural ranking of replica candidates, and no vnode-count tuning. Weighted rendezvous variants support unequal capacity, but their formula should come from a sound specification and distribution tests; casually multiplying scores by a weight can introduce bias.
Both a token ring and rendezvous hashing can be valid. Choose based on node count, lookup frequency, replica-ranking needs, metadata cost, and compatibility with existing infrastructure. More important than the algorithm name is the requirement that every router use the same membership snapshot and the same exact implementation.
6. Partition placement is not replication
A ring answers which owner is responsible for a key. If that owner fails and no copy exists, the data remains unavailable or the cache remains cold. A replicated design may continue around a ring to select distinct physical nodes, or use the next candidates in a rendezvous ranking. Replica count and write policy are separate decisions.
Two vnodes on one machine must not count as two replicas. Likewise, three machines in one rack or availability zone do not tolerate the failure of that domain. Replica placement must understand host, rack, and zone topology, and it must define what happens when the cluster cannot satisfy the requested diversity.
- Partitioning selects the logical shard for a key.
- Replication selects the number and location of copies.
- Consistency defines the required reads and writes and handles versions.
- Failure detection determines when a node is considered unavailable.
Combining all four decisions inside one locate() function makes behavior difficult to test. Keep the placement calculation pure, then let failover and read/write policy consume health information. Given the same snapshot, routing should be deterministic even when the live system is unhealthy.
7. Membership requires epochs and stable identities
The dangerous case begins when routers observe different membership versions. Router A knows about a new node while router B does not, so the same key is written to two owners without a migration protocol. If every process adds or removes nodes from its own health observations, a network partition can produce two internally valid but incompatible rings.
Each placement configuration should contain a monotonically increasing epoch or version, stable node IDs, tokens or weights, lifecycle states, and a checksum. A control plane or consensus-backed store publishes complete snapshots. The data plane validates a snapshot and switches atomically rather than assembling partial updates. IP addresses should not serve as identities because they may be reassigned.
{
"epoch": 42,
"hash_algorithm": "xxh3-64-v1",
"nodes": [
{"id": "cache-a", "state": "active", "weight": 1.0},
{"id": "cache-d", "state": "joining", "weight": 0.25}
],
"checksum": "..."
}
A router should reject a snapshot with an older epoch, log its active epoch, and export a fleet-wide metric showing epoch adoption. Changing the hash algorithm or key canonicalization needs a dual-version migration. Replacing it in place is effectively a full-cluster remap.
8. Rebalancing is a data workflow
Adding a node to placement metadata only defines future ownership. Existing bytes remain on the previous owner. A safe workflow commonly includes states such as joining, streaming, dual-read or dual-write, active, and leaving. Names vary, but read and write authority at every stage must be explicit.
- Calculate the ranges or slots that will move and estimate keys, bytes, and request load.
- Add the destination without full traffic; verify storage, network, and health.
- Copy a range snapshot, then catch up writes that occurred during the copy.
- Validate destination data with checksums, counts, or sampled reads.
- Shift routing in bounded steps while observing errors, latency, and cache misses.
- Delete the old copy or retire the source only after a defined safety window.
A rebuildable cache may not require a complete copy, but its cold start still needs traffic ramping, request coalescing, TTL jitter, and a database load limit. Durable storage cannot treat lazy cache fill as a migration strategy.
The control plane can change an owner in milliseconds while the data plane needs hours to move bytes. A correct design makes this gap explicit.
9. Even distribution does not remove hot keys
A good hash function can distribute key counts evenly while traffic remains highly skewed. A global configuration key, a viral product, or one large tenant may receive thousands of times the median traffic. Because one key normally has one primary owner, consistent hashing still concentrates that hot key on one node.
The response depends on semantics: replicate read-hot data, use layered caches, coalesce identical requests, isolate large tenants, apply controlled salting to aggregatable data, or rate-limit the source. Salting is inappropriate when operations need atomic access to one object. Automatic hot-key replication also needs TTL, invalidation, and fan-out limits so that a read spike does not become a write storm.
Do not monitor only key count. Track request rate, bytes per second, latency, errors, CPU, memory, I/O, and top keys or tenants with appropriate privacy controls. Distribution dashboards should show the maximum-to-median skew, not merely a cluster-wide average.
10. Fixed hash slots and the Redis Cluster example
Instead of placing every key directly against physical nodes, a system can hash keys into a fixed set of logical slots and map those slots to nodes. Redis Cluster uses 16,384 hash slots and the base formula CRC16(key) mod 16384. During resharding, slots move between nodes. Clients maintain a slot-to-node map and handle redirections while configuration changes.
The slot layer provides useful indirection: the logical bucket count remains stable while machine count changes, the control plane can move bounded slot groups, and migration states are easier to represent than a global modulo change. Redis also defines hash tags, where a substring inside {...} can place related keys in the same slot for multi-key operations.
Colocation is a trade-off. Putting every key for a large tenant under one hash tag can create a hot slot. Colocate only data that truly requires a joint operation, impose tenant limits, and test slot distribution using sanitized production-like keys.
11. Test both placement and cluster changes
Unit tests with a few keys are insufficient. Use hundreds of thousands or millions of deterministic inputs to measure distribution, key movement after adding or removing nodes, weighted shares, and cross-language stability. Golden vectors should contain the input bytes, hexadecimal hash, selected token, and expected owner so Java, PHP, Go, and Node.js clients return the same result.
tests:
same key + same epoch -> same owner
different clients -> same 64-bit hash
add one node -> bounded movement
remove one node -> only its ranges move
weighted nodes -> observed share near target
replicas -> distinct hosts and zones
ring wrap-around -> first token selected
stale epoch -> rejected or alerted
Fault tests should cover a node dying during streaming, a router restarting with an old snapshot, temporary control-plane loss, packet loss, a full destination disk, and rollback after only some ranges moved. Test data should approximate real size and popularity distributions; one million equal-size uniform keys will not reveal a shard dominated by a handful of huge or extremely popular objects.
Production checklist
- Key canonicalization, encoding, hash algorithm, and token rules are versioned.
- Nodes have stable IDs; membership snapshots have epochs and checksums.
- Distribution is tested with representative keys and traffic, not only random input.
- Vnode or slot count follows benchmarks and metadata constraints.
- Replica placement avoids duplicate hosts, racks, and zones according to the failure model.
- Joining and leaving workflows separate copying, write catch-up, and routing changes.
- Rebalancing supports rate limits, pause, resume, rollback, and progress metrics.
- Hot keys have separate detection and mitigation.
- Dashboards show epochs, ownership, skew, migration throughput, and redirection errors.
- Golden vectors prove that all clients select the same owner from the same snapshot.
Consistent hashing is valuable because it limits disruption when topology changes, not because it completes a distributed system by itself. A reliable implementation combines deterministic placement with versioned membership, topology-aware replication, and an observable migration workflow. When these pieces are designed together, adding or removing a node becomes a controlled capacity operation instead of a cluster-wide cache storm or a data-routing hazard.




No comments yet. Be the first to share your thoughts.