Overview
Consistent hashing assigns keys to servers so that adding or removing a server moves only a small fraction of keys. With naive hash(key) % N, changing N remaps almost every key; with consistent hashing, only about 1/N of keys move.
It is used by distributed caches, Dynamo-style databases such as Cassandra and DynamoDB, CDNs, and load balancers that need cache affinity.
Picture guests and hosts seated around a circular table. Each guest is served by the next host clockwise. If a new host sits down, only the guests between that host and the previous host change who serves them. Everyone else is unaffected.
When to use it
- Data or cache entries are partitioned across nodes, and nodes are added or removed.
- Remapping many keys would cause a storm of cache misses or data movement.
- You need each key to go to the same node consistently without a central lookup table.
Where it shows up in interviews
Recognize it when: nodes are added or removed often.
- Design a distributed cache
- Design a distributed key-value store
Recognize it when: route the same key to the same server.
- Design a chat server fleet
- Design a CDN's cache placement
Where it is used in real software
Use consistent hashing with virtual nodes to spread data and replicas around a ring.
Routes guilds to servers with consistent hashing so sessions move minimally when nodes change.
Envoy's ring hash and Maglev load balancing keep connections sticky to backends with minimal disruption.
Key terms
- Hash ring
- The hash output space (for example 0 to 2^32 - 1) arranged in a circle.
- Node position
- hash(nodeId) places each server on the ring.
- Key ownership
- A key belongs to the first node found clockwise from hash(key).
- Virtual nodes
- Each physical server appears at many ring positions to spread load evenly.
How it works
- 1Hash the servers
Place each server on the ring at hash(serverId). With virtual nodes, place it at hash(serverId#0), hash(serverId#1), and so on.
- 2Hash the key
Compute hash(key) to find its position on the ring.
- 3Walk clockwise
The first server position greater than or equal to the key's hash owns it; wrap to the start if needed.
- 4Add a server
It takes over only the keys between its position and the previous node counter-clockwise.
- 5Remove a server
Its keys move to the next node clockwise; all other keys stay put.
Keys that move when going from 4 to 5 servers
1,000,000 cached keys
| Strategy | Keys that move | Effect |
|---|---|---|
| hash(key) % N | ~800,000 (80%) | Most of the cache is cold instantly |
| Consistent hashing | ~200,000 (1/5) | Only the new server's share moves |
| Consistent + 150 vnodes | ~200,000, evenly spread | Load taken from all old servers |
NOWStrategy: hash(key) % N | Keys that move: ~800,000 (80%) | Effect: Most of the cache is cold instantly
Modulo hashing changes the owner of a key whenever N changes. Consistent hashing only moves the keys the new node should now own, which keeps cache hit rates high during scaling.
Implementation
import { createHash } from "node:crypto"; const hash = (value: string) => createHash("md5").update(value).digest().readUInt32BE(0); class ConsistentHashRing { private ring: { point: number; node: string }[] = []; constructor(private virtualNodes = 150) {} addNode(node: string) { for (let i = 0; i < this.virtualNodes; i++) { this.ring.push({ point: hash(`${node}#${i}`), node }); } this.ring.sort((a, b) => a.point - b.point); } removeNode(node: string) { this.ring = this.ring.filter((entry) => entry.node !== node); } getNode(key: string): string { const target = hash(key); // Binary search for the first point >= target. let low = 0, high = this.ring.length; while (low < high) { const mid = (low + high) >> 1; if (this.ring[mid].point < target) low = mid + 1; else high = mid; } return this.ring[low % this.ring.length].node; // wrap around }}Complexity and performance
Binary search over N servers x V virtual nodes.
Insert V points into the sorted ring.
K keys, N servers after the change.
Trade-offs
More virtual nodes give smoother distribution but a larger ring. 100 to 200 per server is typical.
Give larger servers more virtual nodes so they own a proportionally larger share.
Consistent hashing balances key counts, not traffic. A celebrity key still lands on one node; replicate or split it.
Variants and related techniques
Store each key on the next R distinct physical nodes clockwise (Dynamo, Cassandra).
For each key, score every node with hash(key, node) and pick the highest. Simple, no ring, O(N) per lookup.
A fast, memory-free algorithm for numbered buckets; it does not support removing arbitrary nodes.
Common mistakes
- No virtual nodes.
Fix: With few servers, random ring positions create very uneven ranges. Use many virtual nodes.
- Using a non-uniform hash like a string length or language hashCode.
Fix: Use a well-distributed hash such as MurmurHash, xxHash, or MD5 truncated.
Interview questions
Why not just use hash(key) % N?
Changing N changes the result for most keys, so nearly the whole cache misses or most data must be moved.
What do virtual nodes solve?
They split each server into many small ranges, which evens out load and spreads a removed server's keys across all remaining servers rather than one neighbor.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Design a distributed cache | Medium | Partitioning and rebalancing. |
| Design a key-value store | Hard | Ring with replication and quorum. |