> ## 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.

# How allocation works

> A group has 10,000 slots. Members own shares of those slots. Each visitor is deterministically assigned to exactly one slot. Whichever member owns that slot is the one they are eligible for; the rest are silently excluded.

Exclusion groups run one step **before** each member's own bucketing.
The math is small and completely deterministic.

## The 10,000-slot space

Every group has an internal bucketing space of **10,000 slots**,
numbered 0 through 9999. That is the whole space; there is no larger
number, no scaling factor.

When you add a member to a group, you give it a **share**, a
non-negative integer of slots it owns. Members can have:

* **Equal shares.** Five members at 2,000 slots each = 10,000 total,
  full coverage.
* **Uneven shares.** One test at 5,000, four at 1,250 each = 10,000
  total.
* **Partial coverage.** Two tests at 4,000 each = 8,000 owned,
  **2,000 unallocated**.

Unallocated space is the visitors who see none of the group's
experiments. That is a valid choice and often a deliberate one.
see [holdouts](/exclusion-groups/holdouts).

## The assignment step

On page load, before per-experiment bucketing runs, the group does
this:

```
slot = murmur3(groupSalt + '/' + userId) mod 10000
```

* `groupSalt`, the group's immutable UUID, generated on creation
  and never changed.
* `userId`, the visitor's stable id, from the `__abtestly_uid`
  cookie (see [bucketing](/developer/bucketing) for how that is
  minted).
* `mod 10000`, coerces the hash to a slot number.

Then: which member owns `slot`? If the slot falls inside a member's
share range, the visitor is **eligible for that one member only**.
Every other member of the group is silently skipped for this visitor.

If the slot is in the unallocated range, the visitor sees **none of
the group's experiments**.

## Slot ranges, worked out

Take a group with three members: A owns 3,000, B owns 4,000, C owns
1,000. Total = 8,000 owned; 2,000 unallocated.

| Slot range    | Owner       |
| ------------- | ----------- |
| 0 – 2,999     | Member A    |
| 3,000 – 6,999 | Member B    |
| 7,000 – 7,999 | Member C    |
| 8,000 – 9,999 | Unallocated |

The order members are listed in the group affects which slot range
each one gets, but not the split proportions. Reordering members
re-dices every visitor because the ranges shift.

## Determinism and stickiness

The hash is deterministic. Given the same `groupSalt` and `userId`:

* The visitor gets the same slot on page one and page 500.
* The same visitor on their next visit still hashes to the same slot.
* Two different visitors get statistically independent slots
  (assuming `userId`s are unique, which they are, UUIDv7).

That is the "sticky membership" property. A visitor bucketed to
member A on their first visit stays on member A forever, unless the
group's membership changes.

## Membership change → generation bump

Every time you add, remove, or change the share of a member, the
group's **generation counter** increments.

Generation is part of the assignment: `murmur3(groupSalt + '/' +
userId + '/' + generation)`. Bumping the generation re-dices every
visitor into potentially different slots.

Why re-dice? Because if you added a new member and the new member
took slots away from an existing member, sticky membership would
lock existing visitors into ranges that no longer belong to their
member. Re-dicing fixes that at the cost of "some visitors switch
which test they see".

The generation shows up on every exposure event (as `gn`) so the
results page can tell you if group membership changed mid-experiment.
The [activity log](/team/activity-log) also records generation
bumps.

## Why exclusion runs *before* per-experiment bucketing

If it ran after, a visitor could be bucketed into experiment A, then
into experiment B, and only *then* the exclusion check would kick
one of them out. The "kick" would happen after A's or B's own
bucketing hash was computed, leaving one experiment's results with
a mysterious "some visitors were bucketed but not exposed" cell.

Running exclusion first cleanly separates the two decisions:

1. Which member of the group is this visitor eligible for? (Group
   decides.)
2. Does the visitor enter *that member's* traffic allocation, and
   which variant? (Member decides.)

Every experiment's stats stay clean.

## Salt and code

If you want to re-derive a visitor's group assignment in a Node
script for an audit, the salt is inside the group's config blob. Ask
support for the salt; the hash is standard MurmurHash3 x86 32-bit,
seed 0.

Reference implementation:

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

function groupSlot(salt, userId, generation) {
  return murmurhash.x86.hash32(`${salt}/${userId}/${generation}`) % 10000;
}

function memberFor(slot, members) {
  let cursor = 0;
  for (const m of members) {
    if (slot < cursor + m.share) return m.key;
    cursor += m.share;
  }
  return null; // unallocated
}
```

Same inputs, same output, every time.

## What does not shift a visitor's group slot

* Cache clear on their browser resets their `userId`, so they hash
  to a new slot, same as it would in any deterministic bucketing.
* Publishing a new site config does **not** change the group
  assignment unless the change touched group membership.
* Adding a new experiment outside the group has no effect at all.

## Next

<CardGroup cols={2}>
  <Card title="Long-term holdouts" icon="pause" href="/exclusion-groups/holdouts">
    Deliberately unallocate a percentage of traffic to measure
    long-tail effects.
  </Card>

  <Card title="Managing membership" icon="users-gear" href="/exclusion-groups/managing">
    Add, retire, and reorder members. What generation bumps do to
    running experiments.
  </Card>
</CardGroup>
