Sticky bucketing

Sticky bucketing remembers a user's variation (one specific version being tested, control or a challenger) assignment. An edit to the rule mid-experiment can never bump someone who is already in it into a different variation. Once a user is placed, they stay there for the life of the rule.

What it is

By default, A vs B uses deterministic hashing: given the same user key, flag key, and rule, a user always lands in the same bucket (sorting a visitor into a group by a consistent hash of their id, so the same visitor always lands in the same group). As long as the rule itself does not change, this alone makes assignments stable.

But a rule can change under a user while an experiment runs. Say a variation split changes: 50/50 becomes 30/70. Or an audience (a named, reusable group of visitors defined by targeting rules) condition gets refined. Or rules get reordered. Any of these can move a user already in the experiment into a different variation, purely from the edit. One change is always safe by itself: growing how much traffic a rule gets. That only ever adds new users; it never reshuffles anyone already inside.

Sticky bucketing protects against every other kind of change. When it is on, the SDK records each user's first assignment. It re-uses that assignment on every later evaluation, no matter what the rule looks like by then.

A worked example

Say your checkout experiment splits traffic 50/50 between the old and new flow. After a week the new flow is clearly winning, so you shift the split to 30% old, 70% new, to get a faster read on the winner.

Without sticky bucketing, some users already in the experiment would silently flip from the old flow to the new one mid-session. These are the ones whose hash sits between the old and new thresholds, right as you are trying to read a clean result. With sticky bucketing on, everyone already assigned keeps what they were first given. Only newly-arriving users see the new 30/70 split.

When to use it

Enable sticky bucketing when any of these apply:

  • You expect to edit the rule while it runs. A changed variation split, a refined audience condition, or a reordered rule can each change which variation a user gets. Sticky bucketing is what protects a user already in the experiment from all three.
  • The experiment involves irreversible actions. Checkout flows, onboarding sequences, and pricing pages are all cases where switching mid-stream would confuse users or corrupt your analysis.
  • Your audience conditions might change. You refine a targeting segment after launch, and you need the users who already qualified to keep their variation.
Info

Assignments are stored for ab_test rules only. A rollout is not an assignment, a holdout is not a decision worth remembering, and a bandit is meant to keep re-deciding as it learns. A stored assignment is still replayed ahead of all of them.

How it works

The SDK stores a StickyAssignment record keyed by user and flag:

TypeScript
import type { RuleType } from '@avsbhq/core'interface StickyAssignmentShape {  /** The assigned variation. */  variationId: string  /** Which rule produced the assignment. */  ruleId: string  ruleType: RuleType  /** Milliseconds since the epoch, when it was assigned. */  assignedAt: number}
TypeScript11 lines

When the evaluator finds a cached assignment, it checks only that the assigned variation still exists on the flag. If it does, the SDK serves that value right away. It does this on purpose, even if the rule that produced it has since been deleted or edited: a stored assignment is designed to survive a rule edit, not be undone by one.

Two things follow from that. If the assigned variation has been removed from the flag entirely, the lookup misses. The SDK then falls back to normal rule evaluation for that read. Separately: the SDK may no longer find the original rule, or that rule may no longer offer the assigned variation. When that happens, it still serves the sticky value, but it logs a warning and skips recording an exposure (the moment a visitor is actually counted in an experiment) for that read. A rule you already deleted does not go on collecting results.

Where assignments are stored

Sticky bucketing is a server-side capability. You pass the store to the server SDK as stickyBucketService, and its read path is synchronous, because evaluation itself is synchronous. The browser client has no sticky store, and no option that takes one: in the browser, bucketing is stable because the visitor id is stable.

StoreWhere it livesUse it for
InMemoryStickyBucketService@avsbhq/nodeA single process, and tests.
RedisStickyBucketService@avsbhq/nodeMulti-process, multi-region deployments.
createDynamoDBStickyBucketService@avsbhq/utilsServerless and Lambda estates already on DynamoDB.
createPostgresStickyBucketService@avsbhq/utilsReusing your Postgres with one small sidecar table.
createDurableObjectStickyBucketService@avsbhq/utilsCloudflare Workers, one Durable Object per shard of users.

Per-SDK usage

The same operation, constructing a server with a sticky store, in three languages:

import { AvsbServer, RedisStickyBucketService } from '@avsbhq/node'import { createClient } from 'redis'const redis = createClient({ url: process.env.REDIS_URL })await redis.connect()const server = new AvsbServer({  sdkKey: process.env.AVSB_SDK_KEY ?? '',  stickyBucketService: new RedisStickyBucketService({ redis, prefix: 'avsb:sticky:' }),})await server.onReady()
TypeScript12 lines

The Redis service (TypeScript sample above) keeps evaluation synchronous. A lookup is either a cache hit or a null, with a background fill, so the first lookup per process misses by design. A store outage degrades bucketing stability and says so in the log. It never breaks an evaluation.

Bringing your own store is two methods, lookup and save, in every SDK.

Tip

Previewing a flag in an admin panel? Use a per-request forced decision: server.forUser(ctx).setForcedDecision('checkout_v2', { variationKey: 'on' }). It takes precedence over the sticky store for that request only, and pairing it with DecideOption.DISABLE_EXPOSURE keeps the preview out of your results.

  • Multi-context identity: using non-user keys for bucketing.
  • Bandits: a bandit keeps re-deciding as it learns, so its assignments are not stored.
  • Holdouts: holdout membership uses its own hash, not the sticky store.
Was this helpful?