Bell Statistics

What is hash-based assignment?

Hash-based assignment computes a unit's variant by hashing its identifier together with the experiment id and mapping the result onto the allocation. It needs no stored state, so every service reaches the same answer independently and at no cost.

Also called
deterministic assignment, MurmurHash bucketing, stateless assignment, hashing users into variants
Allon Korem

Written by Allon Korem

Chief Executive Officer

Last updated

In plain English

The obvious way to assign users to variants is to decide once and write it down. Hash-based assignment does the opposite: it computes the answer from the user's identifier every time it is needed. Hash the identifier together with the experiment id, take the result modulo 100, and map that number onto the allocation ranges. The same inputs always produce the same output, so no state is stored and none can go stale.

That statelessness is worth more than it first appears. A web front end, a mobile client, a recommendation service and a nightly analytics job can each compute a user's variant independently, with no shared database and no network call, and they will agree. There is no assignment table to keep consistent, no read on the critical path, and no cache invalidation problem. For a platform running hundreds of concurrent experiments this is the difference between a viable design and one that adds a database lookup to every request.

The hash function needs specific properties and cryptographic strength is not among them. What matters is uniformity — buckets equally likely — and good avalanche behaviour, meaning a single-bit change in the input scrambles the output. Sequential user ids are the test case: a weak hash can leave neighbouring ids in neighbouring buckets, which correlates assignment with signup date and therefore with tenure. MurmurHash3 is the usual choice; MD5 works and is slower for no benefit here.

The input has three parts and each earns its place. The unit identifier must be stable — a user id where available, a first-party cookie otherwise. The experiment id makes assignments independent across concurrent tests, which is the property most commonly lost by omitting it. And a salt or layer identifier lets a platform re-randomise deliberately, so that a long-running series of experiments does not keep exposing the same users to the same arm.

The one operational hazard is that changing any part of the input reassigns everybody. Rotating the salt, renaming an experiment, or altering how the identifier is derived all move users between arms mid-flight, which is exactly the failure that produces a sample ratio mismatch. These inputs should be treated as immutable for the life of an experiment, and the platform should refuse to change them on a running test rather than trusting anyone to remember.

The formula

One expression, and the properties each component supplies. Everything about this technique is contained in what goes into the hash.

The assignment
variant = allocationMap( hash( unit_id + experiment_id + salt ) mod 100 )

Deterministic and stateless. Every service computing this independently gets the same answer.

What each input buys
unit_id → consistency experiment_id → independence salt → deliberate re-randomisation

Dropping the experiment id correlates assignment across every concurrent test — see bucketing.

Required hash properties
uniform output, strong avalanche, fast

Not cryptographic strength. MurmurHash3 is standard; weak hashes leave sequential ids in adjacent buckets.

The hazard
any change to the input reassigns every unit

Treat the inputs as immutable for the experiment's life — see the chi-square calculator for the SRM check that catches it.

Worked example

A platform team compares two hash functions for assignment quality on 500,000 sequential integer user ids, then simulates the effect of rotating the salt while an experiment is running.

Hash A: MurmurHash3, bucket uniformity
χ² = 104.2 on 99 df, p = 0.34
Hash B: simple modulo of the id
χ² = 0.0, perfectly uniform
Hash B: correlation of bucket with signup date
0.0004 — but adjacent ids adjacent
Hash B: users signing up in one hour
spread over 3 consecutive buckets
Salt rotation mid-experiment
49.6% of users change arm
Resulting SRM after rotation
χ² = 1,840, p < 10⁻¹⁰⁰

The simple modulo produces a perfectly even split and clusters users who signed up together. Rotating the salt reassigns half the population.

Hash B is the instructive failure because its headline check passes perfectly. A modulo of a sequential id gives exactly uniform bucket counts — better than MurmurHash's honest randomness — while placing everyone who signed up in the same hour into a handful of adjacent buckets. Any experiment whose allocation boundaries fall between those buckets is then comparing cohorts rather than random samples, and the SRM check will not notice because the totals are even. That is why avalanche behaviour matters more than uniformity here. The salt rotation row is the operational lesson: it is a legitimate and useful capability between experiments, and inside a running one it silently rebuilds both arms from different people. The platform should make it structurally impossible rather than documented as inadvisable.

Common misconceptions

A cryptographic hash is the safest choice for assignment.
It is slower for no relevant benefit. Assignment needs uniformity and good avalanche behaviour, not resistance to adversarial attack — nobody is trying to forge a bucket. MurmurHash3 and similar non-cryptographic hashes are designed for exactly this profile and are the standard choice in experimentation platforms.
An even split proves the hash is assigning well.
A simple modulo of a sequential id gives a perfectly even split and clusters users who signed up at the same time into adjacent buckets. Uniform totals are compatible with strongly structured assignment. The property that matters is that similar inputs land in unrelated buckets, which even bucket counts do not demonstrate.
Storing assignments in a table is more reliable than recomputing them.
It adds a lookup to every request, a consistency problem between services, and state that can drift. Hashing gives the same guarantee with none of that, which is why it is standard. Storage is genuinely needed only when assignment must survive a change to the hash inputs, which is a rare and deliberate situation.

Frequently asked questions

Which hash function should I use?
MurmurHash3 is the common choice, and anything with good uniformity and strong avalanche behaviour works. Avoid a plain modulo of a sequential identifier, which clusters users who registered at similar times, and avoid cryptographic hashes, which cost speed for a property assignment does not need. Whatever you choose, validate it on your real identifiers rather than on random inputs — sequential ids are where weak hashes fail.
What is the salt for and when should it change?
It lets the platform deliberately re-randomise, so that a long series of experiments does not keep exposing the same users to the same arms. Rotate it between experiments or on a schedule, never during a running test — changing it reassigns roughly half the population mid-flight, contaminating both arms with users who carry behaviour from the other. The platform should enforce that rather than relying on convention.
How do I keep assignment consistent across different services?
Use the same hash function, the same input construction and the same allocation mapping everywhere, and test that independently. The usual failure is subtle divergence — one service concatenating inputs with a separator and another without, or a different integer width — which produces disagreement for a minority of users that is very hard to spot. A shared library, and a cross-service check on a sample of ids, is worth the effort.

Related terms

  • Bucketing

    Three properties assignment must have — random, deterministic, independent — and what breaks when each one fails.

  • Experimentation platform

    Assignment, exposure, analysis and the guardrails — and the last of those is what separates a platform from a flag service.

  • Feature flag

    The switch experiments run on — and the reason a flag that changes mid-test quietly invalidates the result.

  • Traffic allocation

    50/50 is not caution, it is the optimum — and a 90/10 split needs nearly three times the traffic.

Calculate it

  • Chi-square test

    Test a contingency table of counts for association — any number of rows and columns.

Knowing the term is the easy part

Applying it to a live measurement problem is the part that goes wrong. If you are designing an experiment, reading a result you do not trust, or trying to work out what your marketing actually caused, that is the work we do.

References