Skip to main content
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:
  1. 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.
  2. 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.