The question this answers
If I route with hash(key) % N, what exactly happens when N changes?
Uniform distribution of *keys* in expectation, and O(1) routing with no lookup table. Nothing else: no ordering, no range locality, no traffic balance, and — critically — no stability of the mapping when N changes.
Everything below is bought to hold this sentence. "Strongly consistent" with no scope attached is a slogan, not a guarantee — read what it actually covers, and what it explicitly does not.
A router knows the key, the hash function and its own value of N. It does not know whether its N matches the cluster’s. Two routers with different values of N compute different owners for the same key and neither can detect the disagreement from local information alone.
A node knows its own state and the messages that arrived. Everything else is inference from evidence that was already stale. "B has not replied in five seconds" is knowledge; "B is down" is a decision — and usually the bug.
What the hash actually buys
A hash turns any key — a UUID, an email, a tenant name — into a value that is uniformly distributed even when the input is not. That is genuinely valuable: user ids that arrive in blocks, emails clustered by domain, and sequential order numbers all flatten out. Take the hash modulo N and you have a partition number, computed locally, in nanoseconds, with no lookup and no state.
Two properties that matter and are easy to take for granted. The hash must be deterministic across processes and versions — Java’s String.hashCode() is stable, Python’s hash() of a string is randomised per process by default, and using the wrong one gives every process a different partition map. And it must be cheap and well-distributed but need not be cryptographic: MurmurHash3, xxHash and CRC32 are the usual choices; SHA-256 works and is a hundred times slower for no benefit here.
The mapping is also *stateless*, which is its real charm — there is no directory to replicate, no map to keep consistent, nothing to go stale. Everything a router needs is the function and N.
1function partitionOf(key: string, n: number): number {2 return murmur3(key) % n // stateless, O(1), uniform in expectation3}4 5// The mapping is a function of N. Change N and you have changed6// the mapping for almost every key — not just the keys that "should" move.Changing N moves nearly everything
Here is the concrete fact, and it is worth internalising as a number rather than a vibe. A key stays put when h % 4 === h % 5, which happens only when h mod 20 < 4 — that is, for 20% of hash values. Adding one node to a four-node cluster relocates 80% of the data.
The general result: moving from N to N′ leaves roughly 1 / lcm-worth of keys in place, which for the common case of N′ = N + 1 works out to about 1 / N′ staying and 1 − 1/N′ moving. Going from 10 nodes to 11 moves 91% of the keys. Going from 100 to 101 moves 99%. The scheme gets worse as the cluster gets bigger, which is the opposite of what you want from a scaling technique.
And this is not merely a large data transfer. During the reshuffle, every cache in the system is invalidated at once, every node is simultaneously a source and a destination, the network is saturated by the move rather than by traffic, and any client still holding the old N is routing to nodes that no longer own the keys. A capacity expansion becomes an outage.
h h%4 h%5 moved? 0 0 0 . 1 1 1 . 2 2 2 . 3 3 3 . 4 0 4 MOVED 5 1 0 MOVED 6 2 1 MOVED 7 3 2 MOVED 8 0 3 MOVED 9 1 4 MOVED 10 2 0 MOVED 11 3 1 MOVED 12 0 2 MOVED 13 1 3 MOVED 14 2 4 MOVED 15 3 0 MOVED 16 0 1 MOVED 17 1 2 MOVED 18 2 3 MOVED 19 3 4 MOVED stayed: 4/20 = 20% moved: 16/20 = 80%
The fix that is not the ring
Before reaching for consistent hashing, know the simpler fix that many production systems actually use: decouple the number of partitions from the number of nodes. Hash into a large, fixed number of logical partitions — 1024, or 4096 — and then keep a separate, small map from partition to node.
Now hash(key) % 1024 never changes, because 1024 never changes. Adding a node means reassigning some *partitions* to it — moving whole partitions, not rehashing keys. The move is discrete, plannable, resumable, and involves exactly the data you intended to move: with 1024 partitions over 10 nodes, adding an eleventh moves about 1/11 of the data, in units of ~93 partitions.
This is what Kafka does with topic partitions, what Elasticsearch does with primary shards, and what Riak does with vnodes. The cost is that you must choose the partition count up front and it is painful to raise later (changing it *does* rehash), and that you now maintain a map — a small piece of distributed state that must be agreed on and distributed. That is a much better problem to have than an 80% reshuffle.
Consistent hashing (The Ring: Keeping the Mapping Stable When Membership Changes) solves the same problem without a stored map, at the cost of worse balance and a more subtle failure mode. Both are legitimate; the fixed-partition scheme is easier to reason about and easier to operate, and is underrated for it.
| Scheme | Data moved when adding one node | State to maintain | Main weakness |
|---|---|---|---|
| `hash(key) % N`protocol | ≈ 1 − 1/(N+1) — almost everything | None | Any membership change is a full reshuffle |
| Fixed partitions + assignment maptypical | ≈ 1/(N+1), in whole partitions | A partition → node map (small, must be agreed) | Partition count fixed up front; raising it rehashes |
| Consistent hashing ringprotocol | ≈ 1/(N+1), in ring segments | The ring: node positions only | Poor balance without virtual nodes |
What hashing does not fix
Two limitations are structural, and neither is repaired by a better hash function.
Ordering is destroyed. SELECT * WHERE created_at BETWEEN x AND y used a single index scan before; now the rows for that range are uniformly scattered, and the only way to answer is to ask every partition and merge — see Cross-Partition Operations: Paying for What the Split Took Away and Fan Out to 100 and the Component’s Tail Becomes the System’s Median. If ordered scans are a core access pattern, you want Range Partitioning: Scans You Keep, Hotspots You Inherit and its hotspot problems instead.
Traffic skew survives hashing. A hash spreads *keys* evenly. It says nothing about how many requests each key receives. One celebrity account is one key, and one key lands on exactly one partition no matter how good the hash is. Hot Partitions: The Skew Hashing Cannot Fix is a whole lesson because this is the case people expect hashing to solve and it structurally cannot.
A third, softer one: hashing over a key with low cardinality is a trap. hash(country) % 64 gives you at most ~200 distinct destinations and a very lumpy distribution; the number of distinct key values must be *much* larger than the partition count, ideally by two orders of magnitude.
Key points
hash(key) % Nis stateless, uniform in expectation, and catastrophically unstable under changes in N.- Adding one node to N moves about 1 − 1/(N+1) of all keys — 80% at N = 4, 99% at N = 100.
- The scheme degrades as the cluster grows, which is exactly backwards for a scaling technique.
- Fixing the partition *count* and moving whole partitions solves this without a ring, and is what most production systems actually do.
- A hash spreads keys, not traffic. Skew concentrated in one key is untouched by it.
The chain, answered
Every field here is required, which is why no lesson in this domain can recommend a design without naming what an operator sees when it fails, what survives the partition, what repairs it afterwards, and the simpler thing to consider first.
- • Compute
h = hash(key)with a fast, deterministic, non-cryptographic hash. - • Reduce to a partition:
p = h % P, where P is either the node count (the fragile version) or a fixed large partition count (the durable version). - • Look up the node that currently owns partition
p— trivial in the fragile version, a map lookup in the durable one. - • Send the request there. No coordination, no lookup service, no round trip.
- • On a membership change: recompute assignments. With P = N this rehashes everything; with P fixed it reassigns whole partitions.
- • Two processes disagree on N and route the same key to different nodes.
- • Two processes disagree on the *hash function* — a library upgrade, a language default, a signed/unsigned difference — and silently split the keyspace.
- • A negative hash value makes
h % Nnegative in languages where%follows the sign of the dividend, producing a negative partition index. - • The reshuffle after a change in N saturates the network and the change itself becomes the incident.
- • Key cardinality is too low for the partition count and the distribution is lumpy from day one.
- • Expansion outage: adding a node triggers a cluster-wide reshuffle. The operator sees every node simultaneously at 100% network and disk, cache hit rate collapsing to near zero, and p99 latency multiplying for the whole duration of the move.
- • Split-brain routing during a rolling deploy: half the client fleet has N = 5 and half has N = 4. The operator sees a rising rate of "key not found" for data that certainly exists, and writes that appear to vanish and reappear depending on which client wrote them.
- • Language-mismatch corruption: a Python service and a Java service hash the same key differently after a dependency bump. The operator sees duplicate rows for the same logical key, one on each of two partitions, with no error anywhere.
- • Negative modulo crash: an
ArrayIndexOutOfBoundsExceptionor equivalent on a small fraction of keys, appearing only for hash values that happen to be negative, so it looks intermittent and key-dependent. - • Low-cardinality lumpiness: 64 partitions but only 300 distinct key values, so partition sizes differ by 5×. The operator sees permanent, unexplainable skew that does not respond to rebalancing.
- • Routing itself needs none — that is the entire value proposition. No lookup, no lease, no consensus in the request path.
- • Agreeing on N (or on the partition → node map) needs coordination. It is a small amount of state, but disagreement about it is a correctness bug, not a performance one.
- • Because the map is small and changes rarely, it is the classic candidate for a consensus-backed store consulted out of band — see Coordination Services: The Primitives, Not the Product and Cluster Membership: A Belief, Not a Fact.
- • The pattern to imitate: coordinate on the map, never on the request. Coordination in the metadata path is affordable; coordination in the data path is what Coordination Couples Availability is about.
- • If a node holding partition
pis down,pis unavailable — but onlyp. Hashing gives clean, bounded blast radius. - • A stale N does not degrade gracefully: it does not slow down, it routes to the wrong node, and the wrong node may answer confidently and wrongly.
- • Nothing about the hash changes under a network partition; the scheme has no liveness dependency at all, which is genuinely a strength.
- • Detect: a "wrong owner for this key" counter at every node, tagged with the requesting client’s view of N. It is the only way to see a routing disagreement.
- • Contain: nodes must reject keys they do not own rather than serving them. A node that silently accepts a foreign key turns a routing bug into a data-corruption bug.
- • Recover: converge the fleet on one N or one map. For a fixed-partition scheme this is a map push; for
% Nit is a reshuffle you must throttle. - • Reconcile: search for keys that exist on more than one partition — the signature of a period spent with a split hash function or a split N.
- • Verify: sample keys and assert that every process in the fleet computes the same partition for them.
- • Per-partition key count and byte count. Under a good hash these should be within a few percent; a persistent outlier means low cardinality or a broken hash.
- • Per-partition request rate, plotted separately from size. Divergence between the two curves is the early signal of Hot Partitions: The Skew Hashing Cannot Fix.
- • Rate of misrouted requests, broken down by client build. This metric is the difference between finding a routing skew in ten minutes and finding it in a post-mortem.
- • Distribution of hash values themselves, sampled — a cheap sanity check that catches a broken or mis-seeded hash immediately.
- • Point lookups by a high-cardinality key: the overwhelmingly common access pattern for user, session, order and tenant data.
- • Write-heavy workloads with no ordering requirement, where even spread is worth more than locality.
- • When you want no lookup service in the request path and no metadata to keep consistent.
- • When the key distribution is skewed but the *traffic* per key is roughly flat — hashing is exactly the tool for input skew.
- • Range scans and ordered pagination, which become full fan-outs.
- • Clusters that resize often, if you used
% Nrather than a fixed partition count. - • Any workload with a genuinely hot key, where hashing gives a false sense of having solved skew.
- • Multi-tenant systems where one tenant must be isolated or relocated: a hash scatters that tenant across every node, so you can neither isolate its load nor migrate it as a unit.
- • Fix the partition count high and map partitions to nodes explicitly. Same uniformity, movable data, small map — usually the right default.
- • Consistent hashing (The Ring: Keeping the Mapping Stable When Membership Changes) when you want stability without maintaining a map.
- • Range partitioning (Range Partitioning: Scans You Keep, Hotspots You Inherit) when ordered scans matter more than even spread.
- • Directory-based partitioning: an explicit key → partition lookup for the few keys that need special placement, with a hash for everything else. Buys per-key control at the cost of a lookup.
- • A composite key —
hash(tenant) % Pfor placement plus a natural sort key within the partition — to get spread *between* tenants and locality *within* one.
hash(key) % N: changing N moves nearly everything
hash(key) % 4 → key lands on node (hash mod 4) hash(key) % 5 → key lands on node (hash mod 5) measured over 512 keys: moved 414 of 512 (81%) had to move 102 (20%) waste 4.04× the necessary churn
What people believe, and what is true
A better hash function reduces how much data moves when N changes.
It changes nothing. The instability comes from the modulo, not the hash. A perfect hash still remaps ~80% of keys when 4 becomes 5.
Hashing solves hotspots.
It solves *key-distribution* skew. It cannot touch *request-distribution* skew, because one key is indivisible and lands on one partition by construction.
You should use a cryptographic hash for partitioning.
You need uniformity and determinism, not collision resistance against an adversary. MurmurHash3 or xxHash is faster by two orders of magnitude with the same distribution quality.
The reshuffle is fine, it happens once.
It happens on every capacity change, every node loss, and every recovery — precisely the moments the cluster is least able to absorb a self-inflicted load spike.
Go deeper
Only the levels this lesson can honestly fill — a missing level is a claim nobody had.
Overview
hash(key) % N spreads keys evenly and routes with no lookup. Its flaw is that N appears in the formula, so changing the cluster size changes almost every key’s home.
Practical
Never put the node count in the modulus. Hash into a fixed large partition count and keep a separate partition → node assignment. Then a membership change moves whole partitions, at ~1/(N+1) of the data, on a schedule you control.
Advanced
Pin the hash function, its seed and its output width in a written contract shared by every language runtime that routes. The failure when two runtimes disagree is silent divergence: two live copies of the same logical key on different partitions, with no error and no way to tell which is current.
Apply it
- 🔧 Write the twenty-row table above for N = 8 → 9 and confirm the fraction that stays is 1/9.
- 🔧 Take a real key sample from your system, hash it, and plot the per-partition counts at P = 1024. Any partition more than 20% off the mean means your key cardinality is too low.
- ⚡ A cluster is expanded from 6 to 7 nodes during business hours. Within a minute every dashboard is red and the cache hit rate is 3%. Explain precisely what happened and what should have been done instead.
- ⚡ You inherit a system with
hash % Nrouting and cannot take downtime. Design a migration to a fixed-partition scheme that never has two live owners for a key.
- 💬 You route with
hash(key) % 4. The team wants to add a fifth node this afternoon. What do you tell them? - 💬 How would you design the routing so that adding a node moves only 1/N of the data, without using consistent hashing?
- 💬 Two services write to the same sharded store in different languages. What must be true about their hashing, and how would you verify it?