Why do production hash tables grow at roughly 0.6-0.75 occupancy instead of waiting until every bucket is used?
answer
- Two curves moving in opposite directions
- What resource is cheap here, what is expensive
- Compare a bucket slot against a stored entry
- Ask what full even means under chaining
- The cost curve is steep near saturation
basics
~20 sBecause operation cost rises with occupancy long before a table fills. Growing at 0.6-0.75 spends spare bucket slots — cheap next to a stored entry — to keep chains and probe sequences short on every lookup.
solid answer
~50 sThe threshold is a memory-versus-cost dial, and it is set where the cost curve is still flat. Expected lookup work grows with the load factor — roughly proportional to `1 + alpha` under chaining, and much faster than that under open addressing — so the cost of a lookup at 0.95 occupancy is materially worse than at 0.7, while the memory saved is small. The saving is small because a bucket slot is typically one pointer or index, whereas each entry carries a key, a value and per-entry overhead; the bucket array is often a minority of the table's footprint. So you spend 25-40% extra bucket slots, which is cheap, to keep the expensive part — the per-operation probe work on the hot path — near constant. "Wait until it is full" is also incoherent for open addressing, where full means no free slot and insertion cannot complete at all.
go deeper
Know that tables grow before they run out of room, and that the trigger is an occupancy ratio in the region of 0.7. Being able to say the growth is about keeping operations fast rather than about avoiding overflow is enough at this level.
Explain both sides of the dial in numbers: expected work rises with occupancy, while going from 0.75 to 0.95 removes only about a fifth of the bucket slots. Point out that bucket slots are far cheaper than entries, which is why the trade favours headroom.
Show why the configured number sits below where theory bites: real hash functions mix imperfectly, real keys are structured, and the tail of the bucket-occupancy distribution — not the mean — is what a latency percentile measures.
Frame it as a defaults question. One threshold ships to every table in a codebase, so the default must be safe for the worst realistic key distribution, and any per-table deviation needs a measurement and an owner rather than an intuition.
## The threshold is a dial, not a law A hash table's growth threshold — the maximum load factor it tolerates before allocating a bigger bucket array — is a **policy choice**. Nothing breaks at 0.76 and nothing is guaranteed at 0.74. The number lives where it does because of the shape of two curves that move in opposite directions. ## Curve one: cost per operation rises with occupancy Load factor is the mean number of entries per bucket, so it is directly the expected amount of extra work a lookup must do after landing on a bucket. - **Chaining**: expected work per lookup is proportional to `1 + alpha` — one bucket access plus a walk of the expected chain. At 0.75 you expect to inspect well under one extra entry; at 3.0 you expect three. The curve is a straight line, so chaining degrades gracefully. - **Open addressing**: entries live in the bucket array, so a collision forces the key into another key's slot, and lookups walk a probe sequence. Here the expected probe count rises **superlinearly** and diverges as occupancy approaches 1: the last few percent of slots are the expensive ones to find and to fill. Both curves are gentle in the 0.5-0.75 band and unpleasant above it. Setting the threshold inside the gentle region means the table is never operated on the steep part of its own cost curve. ## Curve two: memory saved by running fuller is small Suppose you hold `n` entries. Buckets needed is `n / threshold`: | threshold | bucket slots per entry | |---|---| | 0.50 | 2.00 | | 0.75 | 1.33 | | 0.95 | 1.05 | Going from 0.75 to 0.95 removes about 21% of the bucket slots. But a slot is usually a single pointer or index; an **entry** carries a key, a value, and often a cached hash or a next-pointer. In a table of substantial entries the bucket array is frequently a minority — sometimes a small minority — of total footprint. So the memory you buy back by running hot is a fraction of a fraction, while the latency you pay lands on every single lookup. That asymmetry is the whole argument: **you are trading a cheap resource for an expensive one, so you buy generously.** ## Why "resize when full" is the wrong rule Two separate reasons, and interviewers like both: 1. **For open addressing it is not even reachable.** "Full" means no free slot; the final insertions each scan enormous stretches of the array, and the last one has nowhere to go. The scheme requires headroom to function, not merely to be fast. 2. **For chaining it is undefined.** A chained table has no full — you can always append to a chain. Its cost simply keeps rising linearly. Without a threshold, the structure silently degenerates into an array of linked lists. And the opposite rule — resize on the first collision — is equally wrong, because collisions begin at tiny occupancies. With `m` buckets, the first collision among randomly distributed keys is expected after roughly `1.25 × sqrt(m)` insertions; a 16,384-bucket table sees one around the 160th key. A collision-triggered rule would grow forever. ## Why the band is 0.6-0.75 specifically Several pressures converge there: - Below ~0.5 the memory waste doubles the bucket array for a barely measurable latency gain. - Above ~0.8 the tail of the cost distribution starts to matter even under chaining: the longest chain, not the mean, is what a p99 latency number sees. - **Hash quality is never ideal.** The clean curves above assume uniform scattering. Real keys are structured and real hash functions mix imperfectly, so the effective occupancy in the hot buckets is worse than the scalar suggests. Choosing a threshold below where theory bites leaves headroom for that discrepancy. - The threshold also has to survive **growth granularity**: bucket counts usually double, so occupancy oscillates between roughly half the threshold and the threshold itself. Average realised occupancy is therefore well below the number you configure — another reason the configured number does not need to be aggressive. Mainstream runtimes disagree within this neighbourhood precisely because they resolve collisions differently: a widely used chained table in the Java standard library grows at 0.75, Python's open-addressed dictionaries grow at about two thirds, and C++'s unordered containers permit a maximum load factor of 1.0 by default, leaning on chaining's linear degradation. Same concept, three defensible answers. ## The answer in one breath "Because the expensive resource is time on the lookup path and the cheap resource is bucket slots. Cost rises with occupancy — linearly under chaining, far worse under open addressing — while running from 0.75 up to 0.95 only removes about a fifth of an array of pointers. And 'full' is not a usable trigger: open addressing cannot reach it and chaining has no such point."
- Would you ever deliberately set the threshold as low as 0.5?Yes, when latency dominates memory and the table is small enough that doubling the bucket array is affordable — a hot lookup path in a request handler, or an open-addressed table where the cost curve steepens early. It is also a reasonable defensive setting when the hash is known to mix poorly or the keys are structured, since extra headroom absorbs the clustering that the load factor scalar hides.
- Does raising the threshold change the asymptotic complexity of a lookup?No. Expected lookup stays O(1) and worst case stays O(n) for any fixed threshold below 1 — the threshold is a constant factor, not a complexity change. That is exactly why the decision has to be made with measurements rather than with big-O: what moves is the constant and, far more visibly, the shape of the tail. Interviewers use this to check whether you know what asymptotic notation does and does not capture.
- Why is average chain length a poor predictor of p99 latency?Because the mean is not what a slow request experiences. Under a random scatter, the expected longest chain grows with the number of entries even while the mean stays at the load factor, so a table averaging 0.75 entries per bucket can still have buckets holding five or six. Tail latency is set by those buckets, plus any key skew the average conceals, which is why occupancy distribution beats the single scalar for diagnosis.
saying these in an interview costs you the question
- Says the threshold exists to avoid the table overflowing
- Claims running at 0.95 halves memory use
- Thinks a lower threshold changes lookup from O(n) to O(1)
- Suggests resizing whenever a collision occurs
- Treats full as a meaningful state for a chained table
- Assumes the same threshold suits chaining and open addressing