Bucketing is the algorithm that turns a visitor into a variant. It is
deterministic (same inputs → same output every time), portable (the
same hash on the same inputs runs the same in a Node script), and it
uses no shared state, every visitor’s bucket is computed from their
id alone.
The two-step allocation
ABTestly follows the Amplitude-style two-step model: first decide
whether the visitor enters the experiment, then decide which
variant.
Step 1: allocation
salt, the experiment’s immutable UUID (its bucketingSalt).
Every experiment gets its own salt at creation and it never changes.
userId, the visitor’s stable id (UUIDv7 in localStorage +
cookie).
trafficAllocationBp, allocation in basis points (10000 = 100 %).
If enter is false, the visitor is not in the test. The exposure event
does not fire. The visitor sees the original page.
Step 2: variant pick
Weights are also in basis points and must sum to 10000. The variant
whose cumulative weight range contains hash2 wins.
The hash
MurmurHash3 32-bit, x86 variant, seed 0. Well-known, fast,
uniformly distributed for short strings.
The two different suffixes (/Exposure and /Bucket) mean the
allocation decision and the variant decision are independent.
increasing allocation to 60 % does not shuffle which visitors get
which variant among those already inside.
Why the salt is per-experiment
Two reasons:
- A visitor that “always sees Variant A” would be wrong. If two
experiments shared a salt, a visitor bucketed to variant 0 on
test 1 would also be bucketed to variant 0 on test 2. That is a
subtle correlation you almost never want. Per-experiment salts
break that dependency.
- Cloning generates a new salt on purpose. So a visitor that saw
Variant B in the original does not automatically see Variant B in
the clone. Cleaner reads on both.
The salt is exposed nowhere, it lives inside the experiment’s
config blob. If you need to re-derive a bucket in a Node script for an
audit, ask us for the salt and we will look it up.
Where userId comes from
__abtestly_uid, a UUIDv7 written to both localStorage and a cookie
on first visit.
The cookie domain is resolved to eTLD+1 via a public-suffix probe, so
www.acme.com and blog.acme.com share a visitor id when both use
ABTestly on the same registrable domain.
Clearing either the cookie or localStorage produces a new id. Visitors
who clear cookies weekly are re-bucketed weekly; there is no way
around that without something like fingerprinting, which we do not do.
Reproducing a bucket in Node
Same numbers, same variant, every time.
Mutual exclusion groups
Bucketing inside an exclusion group is a
step before per-experiment bucketing. First the visitor is assigned
a slot in the group’s 10000-slot space; whichever member owns that
slot is the one the visitor is eligible for. Then the standard
two-step runs for that one member.
What could shift a visitor’s bucket
- Cleared cookies/localStorage. New id, new bucket. This is
visible-to-user and expected.
- Cloned experiment. New salt, so re-bucketed. This is expected
and desirable.
- Reset data on the experiment. New epoch, but same salt, the
bucket does not shift.
- Weights changed on the experiment. For a visitor already
inside, the variant they get depends on the new weights. See
Variations for why weight changes on running
experiments are gated.
Nothing else shifts a visitor’s bucket. If you see a visitor “moving
between variants” without one of the above, that is a bug and we want
to hear about it. Last modified on August 1, 2026