In plain English
Bucketing is the mechanism that puts each unit into a variant. It sounds like plumbing and it is the foundation everything else rests on: if assignment is not what it claims to be, every downstream statistic describes something other than the experiment you thought you ran. Three properties are required, and each fails in a characteristic way.
It must be random with respect to everything that predicts the outcome. That is what makes the arms comparable and is the entire basis for reading a difference as an effect. Assignment by user id parity, sign-up order, or anything else with structure will correlate with tenure or behaviour and produce a confounded comparison that looks like a clean one.
It must be deterministic for a given unit. The same user must land in the same variant on every request, every session and every device, or their experience flickers and their data belongs to both arms. This is why hash-based assignment is the standard implementation — it computes the bucket from a stable identifier rather than storing a decision, so it is consistent without needing a lookup and works identically across services.
And it must be independent across concurrent experiments. This is the property most often broken, by hashing only the user id rather than the user id combined with the experiment id. When that happens, the same users land in the same relative position in every experiment: a user in the top decile of the hash space is in variant B of everything. Assignments become correlated across tests, so interaction effects stop being random noise and become systematic, and two experiments running together can be measuring each other.
The verification is straightforward and belongs in the platform rather than in anyone's memory. A sample ratio mismatch check on every experiment catches uneven splits and mid-flight allocation changes; a periodic A/A test catches the subtler failures in metric computation and variance. Both are cheap, both run automatically, and between them they cover most of what can go wrong here.
The formula
The standard implementation is one line, and the detail that matters most is what goes into the hash.
- The assignment function
bucket = hash( unit_id + experiment_id ) mod 100Deterministic, uniform, and independent per experiment. The experiment id is what delivers the third property.
- What omitting the experiment id does
same user → same bucket in every experimentAssignments correlate across concurrent tests, making interactions systematic rather than averaging out.
- Mapping bucket to variant
variant = first v where cumulative allocation > bucketChanging allocations mid-test moves these boundaries and reassigns users — see traffic allocation.
- The verification
chi-square goodness of fit against the intended splitRun on every experiment, at a strict threshold — see the chi-square calculator.
Worked example
A platform hashes only the user id, not the user id plus experiment id. Three experiments run concurrently over the same 200,000 users. Each reports a clean sample ratio and each looks healthy in isolation. An analyst checks how assignments relate across the three.
- Experiments running
- 3, all 50/50
- SRM check, each experiment
- passes (p > 0.4)
- Users in variant of all three
- 99,847 of 200,000 (49.9%)
- Expected if independent
- 25,000 (12.5%)
- Correlation between assignments
- 1.00
- Users in a mixed combination
- 0
Every user is in the same relative position in all three experiments. There are only two distinct populations, not eight.
Each experiment passes its own health check because each split is genuinely 50/50 — the failure is invisible from inside any single test. What has actually happened is that the three experiments are running on two groups of users rather than eight, so no combination of treatments is ever observed except all-control and all-treatment. If any two of the three interact, that interaction is baked into all three results and attributed to whichever change the reader is looking at. The fix is one token in the hash input. The reason this is worth a worked example is that it produces no error, no imbalance and no alert: the only way to find it is to check the cross-tabulation between concurrent experiments, which almost nobody does unless they know to look. Adding a periodic assignment-independence check alongside the SRM check is cheap insurance against a class of failure that otherwise persists indefinitely.
Common misconceptions
- דAny hash function will do as long as the split comes out even.”
- An even split is necessary and not sufficient. The hash must also distribute independently of user characteristics — some fast hashes have poor avalanche behaviour and correlate with structured inputs like sequential ids. MurmurHash and similar non-cryptographic hashes designed for uniformity are the standard choice for good reason.
- דStoring each user's assignment in a database is safer than computing it.”
- It introduces a lookup on every request, a consistency problem across services, and a migration whenever allocations change. Hashing gives determinism for free and identically everywhere, which is why it is standard. Storage is occasionally necessary — when assignment must survive a change to the hash input — and it is the exception rather than the safer default.
- דIf each experiment shows a balanced split, bucketing is working.”
- Balance within each experiment says nothing about independence across them. Hashing without the experiment id produces perfectly balanced individual splits and perfectly correlated assignments, so concurrent tests silently share populations. Checking the cross-tabulation between experiments is what catches it.