9 min read · Guide · Data structures
How it works · Data structures

How a hash table turns a key into an answer in O(1).

A function that turns any key into an array index. Constant time on average and linear time at worst, so most of the engineering goes into keeping the average case.

Parts01–08 InteractiveLive insertion PrereqArrays / hashing

What is a hash table?

A hash table is an array of buckets indexed by a hash function applied to the key. Hans Peter Luhn proposed the idea at IBM in 1953. Today they back the map / dict / HashMap in most languages, many database indexes, caches and router flow tables. The two collision strategies are chaining (linked lists per bucket) and open addressing (probe to the next bucket).

The idea is simple. Take a key (a string, a number, anything hashable) and apply a hash function to turn it into an integer. Take that modulo the table size to get the index where the key's value lives. A lookup repeats the same hash and fetches from the same index in O(1), with no search or traversal.

The hash function maps an unbounded keyspace onto a bounded number of slots, so two keys can hash to the same slot. That is a collision, and the strategy for resolving collisions largely defines how a given table behaves.


Three collision strategies filling the same table

Below is an 11-slot hash table. Type a key to see where it lands. Switch strategies and reseed to get the same keys in different layouts. Open addressing keeps everything in the slots, while chaining grows lists at busy slots.

0 / 11 slots · load factor 0.00
0 —
1 —
2 —
3 —
4 —
5 —
6 —
7 —
8 —
9 —
10 —

Open addressing vs chaining: two ways to handle collisions

When two keys collide, the table can either find another empty slot in the same array (open addressing) or turn the slot into a list and append (chaining). Both work, with different operational trade-offs.

Open addressing

Everything in the array

No pointers, cache-friendly, simple memory layout: every key sits in the same flat array. The cost is sensitivity to load factor, since probe distances grow quickly as the table fills. Linear probing has clustering issues; quadratic and double hashing scatter better. Used by Python dict, Java's IdentityHashMap.

Chaining

A list per slot

Each slot holds a linked list (or small array). Collisions extend the list rather than spilling into other slots. It tolerates higher load factors, but costs memory for pointers and loses cache locality. Used by Java's HashMap, Go's map.


Load factor and when the table has to grow

The load factor is occupied slots divided by total slots. Past a threshold (typically 0.75 for chaining, 0.5 for open addressing), performance starts to fall off. The fix is to allocate a bigger array, walk every key, rehash with the new size, copy.

Rehashing is O(n) and pauses the table. Java doubles the table each time it crosses the threshold, so resizes are rare and amortise to O(1) per insert. Some implementations rehash incrementally, moving a few keys per operation instead of all at once, to keep p99 latency down. Redis does this for its dictionary.

Pick a prime size

When the table size is prime, hashes mod size distribute more evenly. Powers of two are cache-friendly but require a stronger hash function to avoid patterns. Most modern implementations use a power-of-two size and rely on the hash function, while older code often keeps the prime-size convention.


What makes a good hash function

A good hash function spreads inputs uniformly across the output range, changes a lot when one bit of input changes (avalanche), and runs fast. Modern non-cryptographic hashes (xxHash, MurmurHash, CityHash) do all three in a few ns per key.

Cryptographic hashes such as SHA-256 are too slow for hash tables and the extra bit quality isn't needed. Java's String.hashCode() is simple and can cluster on real-world strings, which is why HashMap mixes the high bits into the low bits before indexing.


HashDoS: when an attacker forces every key to collide

If your hash function is deterministic and the attacker knows the algorithm, they can craft keys that all collide. A web server putting JSON keys into a hash map is suddenly doing O(n²) work per request. This is HashDoS, and it affected major frameworks until they fixed it.

The usual fix is a per-process random seed, so every server starts with a different hash function. Python, Ruby, Rust and Go do this. The attacker can't see the seed, so precomputed colliding keys no longer work. Java instead limits the damage by converting long chains to trees.


Where hash tables show up, usually out of sight

Most languages implement their map / dict / object as a hash table. Database query optimizers use them for joins. Caches use them for keys. JVM string interning uses one. Operating system kernels use them for several internal tables.

memcached

Distributed cache

Each node holds a hash table, and clients spread keys across nodes with consistent hashing.

postgres

Hash join

Build a hash table on the smaller relation, probe with the larger. The optimizer picks this when the build side fits in memory and the join is on equality. See also database indexing for the B-tree alternative.

javascript

Object

An object is a hash table from string keys to values. V8 uses hidden classes for stable shapes and dictionary mode for dynamic ones, and dictionary mode is a hash map.


What slows a hash table down

mutable keys

Don't mutate keys

Modifying a key after it's in the table changes its hash: the old slot still references it, but lookups from the new hash miss. The table appears corrupt. Many languages restrict mutable keys; Python, for example, requires hashable types such as tuple or frozenset.

low-quality hash

Bad clustering

If your hash function maps similar inputs to similar outputs, sequential keys land in the same area and probe distances explode. Use a known-good hash, or test a custom one for distribution.

resize storms

Pre-size when you can

If you know you'll insert a million keys, allocate the table at that size up front. Growing from the default 16 means about 16 rehashes, which creates a lot of garbage for the collector and costs CPU.

How Java, Python, Go, and Rust build their hash maps

Java HashMap
Chained buckets. Since Java 8, a chain longer than 8 entries (in a table of at least 64 buckets) converts to a red-black tree for O(log n) worst case. Initial capacity 16, load factor 0.75.
Python dict
Open addressing with perturbation. Keys keep insertion order (an implementation detail in CPython 3.6, guaranteed since 3.7), implemented with two arrays: a sparse index table and a dense entries list. Load factor caps at ~2/3.
Go map
Buckets of 8 entries with overflow buckets in the classic implementation, with a Swiss-table design since Go 1.24. Resizing grows capacity and rehashes incrementally during writes.
Rust HashMap
SwissTable algorithm (Google, 2017): single array of 16-byte groups, SIMD lookup of 16 metadata bytes at once. Typically faster than chained tables. It has been the default since Rust 1.36.
C++ std::unordered_map
Effectively chained, because the standard requires reference stability, which open addressing breaks. Performance lags SwissTable variants (Abseil's flat_hash_map, F14 from Folly) by ~2× in benchmarks.
Postgres hash index
On-disk hash index. Optimised for equality lookup but rarely used in practice: B-tree handles equality fast enough plus range, so most workloads pick B-tree by default.

SwissTable. Google's 2017 CppCon talk introduced SwissTable, which uses SIMD to compare 16 metadata bytes at once. Rust, Abseil (Google), F14 (Meta) and recent Go versions use variants. The Java and Python standard-library hash maps do not use it.


When hash collisions became a security vulnerability

The original (2003). Crosby and Wallach showed that an attacker who can choose hash table keys (e.g., via HTTP form fields parsed into a server-side hash) can craft inputs that all collide into the same bucket. Insertion becomes O(n²); a 1,000-key form takes seconds to parse. The problem went largely unaddressed for years.

2011: the sequel. Klink and Wälde at the 28C3 conference demonstrated practical exploits against PHP, Java, Python, Ruby and ASP.NET. A few KB of carefully crafted HTTP form data could pin a server's CPU at 100% for minutes. Languages responded with randomised hashing, and SipHash (Aumasson and Bernstein, 2012) became a common choice. It takes a per-process key, so attackers can't predict the input-to-bucket mapping.

Today. Most languages randomise the hash used by their built-in tables. Python has used SipHash since 3.4 (2014), Rust's default hasher is SipHash, and Go uses a randomised AES-based hash. Java does not randomise, and relies on tree buckets to cap the worst case. The practical rule is to seed hashes that process untrusted input.



A closing note

A hash table needs three pieces: a function from key to index, a strategy for collisions, and a threshold for resizing. The literature is deep, but those three cover most of what you need to write or tune one.

Found this useful?

Published Updated