skip to content

After a config reload, a stored key starts hashing to a new value — why does its map entry become unfindable even though no key field changed?

level: seniorimportance: nice to knowfreq 30%

answer

  1. iteration finds it, lookup does not
  2. where it was stored versus where we now look
  3. the hash read something beyond the key's fields
  4. consistency: same key, same hash, while stored

basics

~20 s

The entry still sits in the bucket the old hash chose, but every lookup now computes the new hash and probes a different bucket. The contract's consistency clause requires a stored key's hash to stay stable, so hash inputs must never include external state like configuration.

solid answer

~50 s

The equality-hash contract has a consistency clause: a key must hash to the same value for as long as it is stored. Here nothing about the key changed — the hash function's *other* inputs did. It read normalization rules or similar state from configuration, and the reload changed them, so the entry is stranded in the bucket chosen at insert time while lookups probe the bucket the new hash points at. The diagnostic signature is distinctive: a full iteration still lists the entry, but a keyed lookup misses — storage and routing disagree. The fix is to derive the hash purely from the key's own stable fields and rebuild the table by re-inserting every entry. One scope note: per-process hash randomization in production hash maps is legal, because the seed is fixed for the table's lifetime — its corollary is simply never to persist hash values or compare them across processes.

go deeper

for a junior

Be ready to state the consistency requirement — a key must keep hashing to the same value while it is stored — and that violating it makes entries unreachable without any error.

for a middle

Explain the mechanics: stored under the old hash's bucket, probed under the new one, and the iteration-finds-it-lookup-misses signature that identifies the class of failure.

for a senior

Demonstrate operational judgment: trace the drifting input to external state, know that per-process seeding is legal while config-dependent inputs are not, and plan the iterate-and-rebuild recovery.

for a principal

Own the policy line: hash values are ephemeral process-local numbers, never persisted or compared across processes or versions, and a key type's hash inputs are part of its audited, frozen surface.

## The consistency clause The well-known half of the equality-hash contract says equal keys must hash equal. The quieter half says a key's hash must be **consistent**: the same key must produce the same hash value for as long as any table holds it. A hash table records nothing but "this entry lives in the bucket its key hashed to at insert time." Every later operation re-derives that location by hashing the key again. If the second derivation disagrees with the first, the table's only piece of navigation is wrong — the entry is physically present and logically unreachable. ## How a hash drifts without the key changing The famous version of this failure is mutating a key's own fields after insert — a story of its own. The subtler version, the one in this support ticket, involves **no change to the key at all**: the hash function consumed inputs beyond the key's fields. Concretely: a service interns lookup keys for catalog labels and, to make matching "smart", its hash and equality normalize text using rules loaded from a reloadable configuration file — locale folding tables, alias maps, strip-lists. At insert time the rules map the key to one canonical string, which hashes to bucket 40. An operator hot-reloads the config; the folding rules change slightly; the same key now canonicalizes differently and hashes to bucket 12. Every entry inserted before the reload is stranded: present in the table, invisible to lookups. Anything non-local can smuggle itself into a hash this way: configuration, environment variables, locale settings, a lazily computed field, wall-clock-derived state. The rule that prevents the whole class: **a hash may read only the key's own stable value — nothing that can change while the key is stored.** ## The diagnostic signature This bug has a fingerprint worth memorizing, because it separates it from a plain missing entry: - **Iteration finds it; lookup does not.** Walking the table's buckets visits every entry regardless of hash, so a full scan or a size count shows the "missing" data. A keyed lookup, which trusts the hash, misses. - Counts disagree with membership: the table reports n entries while n keyed lookups succeed for fewer. When a report says "the entry is there when we dump the map but a get returns nothing," the hash-at-lookup differs from the hash-at-insert — either the key mutated, or, as here, the hash function's other inputs moved. ## What randomized hashing does and does not break Production hash maps deliberately randomize their hashing per process — typically a keyed function such as SipHash with a seed drawn at startup — to stop adversaries from precomputing colliding keys and degrading the table to O(n). Python randomizes string hashes per process, and Ruby does likewise; several other runtimes vary iteration order run-to-run for the same reason. This is **contract-legal**: the seed is fixed for the process, so any key hashes identically for as long as any table in that process holds it. Consistency is scoped to the table's lifetime, not to eternity. The corollary is the discipline it imposes: hash values are ephemeral, process-local numbers. Never persist them to disk, send them across the network, or compare them across runs — the next process will derive different ones for the same keys. Systems that need a durable digest of a key must compute one with an explicitly stable, versioned function, separate from the table's hash. ## Recovering and preventing Once entries are stranded, the table cannot heal in place — it does not know which buckets hold misplaced entries. The recovery is a rebuild: iterate everything (iteration still works), re-insert into a fresh table under the corrected, stable hash. Prevention is a design rule plus a test: hash and equality read only the key's immutable value; and a soak test that inserts, flips every reloadable input, and asserts lookups still hit.

  • Production hash maps randomize their hashing per process — doesn't that violate the consistency requirement?
    No. The seed is drawn once at startup and fixed for the process, so every key hashes identically for as long as any table holds it — consistency is scoped to the table's lifetime, not across runs. What it does forbid is treating hash values as durable: never persist them, transmit them, or compare them between processes, because the next run derives different values for the same keys.
  • What is the diagnostic signature that distinguishes this from an entry that was simply never inserted?
    Iteration versus lookup disagreement. Walking the buckets visits every entry regardless of hash, so a full dump or size count shows the data; a keyed lookup, which trusts the current hash, misses it. An entry that was never inserted is absent from both. When the dump shows it and the get does not, hash-at-lookup differs from hash-at-insert.
  • Once entries are stranded, what is the fix?
    Rebuild, not repair. The table cannot locate misplaced entries — its only navigation is the now-wrong hash — but iteration still visits everything, so you iterate the old table and re-insert every entry into a fresh one under a corrected hash that reads only the key's own stable fields. Then remove the external input from the hash so the class of bug is gone.

A warehouse re-labels its aisles overnight: the crate is still on the shelf where it was put, but every picker now walks to the aisle the new labels point at and reports the crate missing.

saying these in an interview costs you the question

  • If the entry is still physically in the table, a lookup must find it
  • Per-process hash randomization breaks the consistency requirement
  • Persisting hash values is safe because the hash algorithm is deterministic
  • Only mutating the key itself can strand an entry

context