Skip to content
Select theme

Why is a HashMap's order different every run?

Short answer: a HashMap records no order, and the RandomState hasher behind HashMap::new() is randomly seeded and re-keyed for every new map, so the same inserts come back in a different sequence in every run. When the output has to be stable, sort the keys or use a BTreeMap.

From a fresh project — cargo new hash-order — replace the contents of src/main.rs with each block below and run it with cargo run. The project name shows up only in cargo’s own output; nothing else here depends on it.

A hash map keeps entries in buckets chosen by the hash of each key. Nothing records the order the keys arrived in, and iteration walks the table, so the sequence you get back is a property of the hashes and the table, not of the inserts. The standard library’s own docs for iter, keys and values say the same thing in one phrase: “in arbitrary order”.

Two runs of that same binary, back to back, on this machine (Rust 1.99.0, edition 2024). The first:

Margaret: 8
Barbara: 7
Grace: 5
Katherine: 9
Ada: 3
sorted: ["Ada", "Barbara", "Grace", "Katherine", "Margaret"]

Same binary, same machine, second run:

Katherine: 9
Ada: 3
Grace: 5
Margaret: 8
Barbara: 7
sorted: ["Ada", "Barbara", "Grace", "Katherine", "Margaret"]

The last line never moves; the map’s order does.

HashMap::new() builds a RandomState, and the standard library’s documentation for HashMap is direct about why: the default algorithm is “selected to provide resistance against HashDoS attacks” and is “randomly seeded”, currently SipHash 1-3. If the seed were fixed, anyone could pre-compute keys that all land in one bucket and turn every lookup into a scan.

The seed is per map, not per process. Each RandomState::new() takes a per-thread pair of random keys and increments one of them, so the next map on the same thread gets different keys. Two maps built one line apart disagree:

["Margaret", "Ada", "Katherine", "Barbara", "Grace"]
["Ada", "Katherine", "Grace", "Margaret", "Barbara"]
same order: false

Both lines differ from run to run, and the two never agreed across the five runs behind this page. Within one run the map is stable — iterating it twice gives the same sequence — and two maps sharing one RandomState (HashMap::with_hasher(state.clone())) do agree.

Sort when you print. The last line of the first block is the pattern: collect the keys, sort(), print. The map stays a HashMap, and the cost is one sort per output.

Or use a BTreeMap. Sorted iteration is the structure, not a step:

Ada: 3
Barbara: 7
Grace: 5
Katherine: 9
Margaret: 8

The trade-off is the one from the HashMap versus BTreeMap article: lookups move from expected O(1) to O(log n), and keys need Ord instead of Hash + Eq.

Or keep the HashMap and fix the hasher. BuildHasherDefault<DefaultHasher> seeds SipHash with fixed keys, so every run of that binary lays the table out the same way:

Grace: 5
Katherine: 9
Ada: 3
Barbara: 7
Margaret: 8

Two caveats, both from the standard library’s own documentation. The default hasher’s algorithm “is not specified, and so it and its hashes should not be relied upon over releases” — a future compiler may order the table differently. And the random seed is the HashDoS protection; with fixed keys it’s gone, so this is the wrong tool when the keys come from untrusted input.

Next in this chapter: how to count word frequency with a HashMap.