> ## Documentation Index
> Fetch the complete documentation index at: https://docs.abtestly.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Bucketing model

> How ABTestly assigns a variant. MurmurHash3, a two-step allocation, a per-experiment salt. Deterministic and portable, you can re-derive any visitor's bucket from the same inputs.

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

```
hash1 = murmur3(salt + "/" + userId + "/Exposure") mod 10000
enter = hash1 < trafficAllocationBp
```

* `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

```
hash2 = murmur3(salt + "/" + userId + "/Bucket") mod 10000
variant = the variant whose cumulative weight range contains hash2
```

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

```js theme={null}
import murmurhash from 'murmurhash3js';

function bucket(salt, userId, trafficAllocationBp, weights) {
  const hAllocation = murmurhash.x86.hash32(salt + '/' + userId + '/Exposure') % 10000;
  if (hAllocation >= trafficAllocationBp) return null; // not in test

  const hVariant = murmurhash.x86.hash32(salt + '/' + userId + '/Bucket') % 10000;
  let cum = 0;
  for (const { key, weight } of weights) {
    cum += weight;
    if (hVariant < cum) return key;
  }
  return weights[weights.length - 1].key; // never happens if weights sum to 10000
}
```

Same numbers, same variant, every time.

## Mutual exclusion groups

Bucketing inside an [exclusion group](/targeting/exclusion-groups) 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](/build/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.
