Consistent hashing is the routing function that keeps distributed caches, databases, and pub/sub systems evenly balanced while nodes join and leave. Instead of mapping keys to shards with modulo arithmetic—which causes full reshuffles on membership changes—consistent hashing arranges nodes on a logical ring so only a narrow slice of keys move.
The approach underpins distributed key-value stores, vector search shards, and most CDN edge caches. Virtual nodes provide fine-grained control over load distribution, helping ops teams rebalance hot partitions without copying petabytes.
This lesson walks through the structure, failure handling, and implementation patterns you need to apply consistent hashing to new services, including how to layer replication factors and locality hints on top of the basic ring.
A membership change only affects the key ranges between the departing node’s previous position and its successor, typically < 1/ring_size of total data.
Virtual nodes allow weighting: assign more positions to beefier machines so the ring automatically hands them more traffic.
Each node chooses v tokens by hashing node_id || replica_index. Tokens are stored in a sorted array. To route a key, hash it, binary search for the first token greater than the hash, and fallback to index 0 if you wrap around.
To replicate, continue walking clockwise until you collect rf distinct physical nodes. Keep track of rack/region metadata and skip nodes that would violate anti-affinity rules. This yields a stable preference list used by Raft groups in an experimental distributed database.
Rings can be weighted per keyspace: maintain separate token sets for write-heavy and read-heavy workloads to prevent one skewed dataset from overwhelming the rest.
type Token struct {
Value uint32
Node string
}
func buildTokens(nodes []Node, virtuals int) []Token {
tokens := make([]Token, 0, len(nodes)*virtuals)
for _, n := range nodes {
for v := 0; v < virtuals; v++ {
h := hash32([]byte(fmt.Sprintf("%s-%d", n.ID, v)))
tokens = append(tokens, Token{Value: h, Node: n.ID})
}
}
sort.Slice(tokens, func(i, j int) bool { return tokens[i].Value < tokens[j].Value })
return tokens
}
function locate(tokens: Token[], key: string, rf = 3): string[] {
const hash = murmur(key)
const owners = new Set()
let idx = binarySearch(tokens, hash)
while (owners.size < rf) {
owners.add(tokens[idx].node)
idx = (idx + 1) % tokens.length
}
return Array.from(owners)
}