Sticky assignment
Splitstream's sticky-forever rule: once a unit has been bucketed into a variant for an experiment, that variant is the answer for the lifetime of the experiment. Re-bucketing a returning user mid-experiment poisons the data — the statistics no longer hold because the unit is now contributing to multiple variant arms.
How it's enforced
Sticky is enforced at the database level, not the cache level. The first /v1/assign call for a (experiment, unit) pair writes a row to the partitioned assignments table with primary key (experiment_id, unit_id). Subsequent calls read that row. The Redis cache is a performance optimization; the row is the source of truth.
assignments is hash-partitioned by experiment_id into 16 children for write distribution. Per-partition secondary index on unit_id makes the SDK reconciliation endpoint (GET /v1/assignments/:unit_id) a cheap index seek per partition rather than a heap scan.
Mid-experiment weight changes
Mid-experiment weight changes are a documented anti-pattern. They are also supported, because they happen in real product work. The admin requires a confirmation modal and writes a dedicated weight_changeaudit entry. The results page surfaces "weights changed at T+5 days" inline next to the posterior chart — the credible-interval interpretation depends on it.
Critically, changing weights does not re-bucket units that have already been assigned. The new weights only apply to the next unseen unit. The old units stay in their original variant. This means the realized traffic split deviates from the configured split — a Sample Ratio Mismatch test that excludes the weight-change boundary may run hot. The audit log makes the deviation diagnosable.
Force-rebucket atomicity
DELETE /v1/assignments/:experiment/:unit_id deletes the assignment row and sets superseded_at = now() on every exposure event for the unit-experiment pair. The analysis worker filters tombstoned rows (WHERE superseded_at IS NULL). Without the tombstone, the unit's data appears in both the old variant's and the new variant's metrics — the analysis is silently wrong.
Holdouts and mutex groups
Mutex-group enforcement is a special sticky case: the first call into any experiment in the group writes a row to mutex_group_assignments claiming the unit. Subsequent calls within the group for that unit return a mutex_holdout reason and a null variant. The row is written in a SELECT FOR UPDATE transaction to race-protect simultaneous calls.