Sticky Bucketing
Sticky bucketing persists a user's variation assignment so that datafile updates — traffic re-allocations, audience condition changes, rule reorders — never cause mid-experiment reassignment. Once a user is placed in a variation, 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 traffic allocation, a user always lands in the same bucket. This means that as long as you do not change the allocation or rule order, assignments are implicitly stable.
But deterministic hashing only guarantees stability when the rule itself does not change. If you increase traffic from 20% to 50%, users who were previously outside the experiment can enter it — and some existing users can shift buckets. Sticky bucketing prevents this: when enabled, the SDK records each user's first assignment and re-uses it on every subsequent evaluation, ignoring any bucket drift caused by rule changes.
When to use it
Enable sticky bucketing when any of these conditions apply:
- You expect to ramp traffic during the experiment (e.g., 10% → 25% → 50% over several days).
- The experiment involves irreversible user actions — checkout flows, onboarding sequences, pricing pages — where switching mid-stream would confuse users or corrupt your analysis.
- Your audience conditions might change (e.g., you refine a targeting segment after launch) and you need previously qualified users to keep their variation.
ab_test and bandit rules. Targeted delivery rules are never sticky — they are intended to change user behaviour deliberately.How it works
The SDK stores a StickyAssignment record keyed by user + flag:
1interface StickyAssignment {2 variationId: string // the assigned variation3 ruleId: string // which rule produced the assignment4 ruleType: 'ab_test' | 'bandit'5 assignedAt: number // ms-epoch timestamp6}When the evaluator encounters a cached assignment, it checks whether the stored ruleId still exists in the current datafile. If the rule has been deleted or the user no longer matches its audience, the SDK logs a 'rule_no_longer_matches' warning via the logger and falls through to normal evaluation — it does not silently serve a stale variation.
Storage backends
The StickyBucketService interface is synchronous on the read path (hot path during evaluation) and can be backed by any of the following built-in adapters from @avsbhq/utils/storage:
- InMemory — default, no persistence across restarts. Suitable for development or single-instance servers.
- Redis —
createRedisAdapter. Recommended for multi-instance Node.js deployments. - DynamoDB —
createDynamoDBAdapter. Good for serverless/Lambda environments already using DynamoDB. - Postgres —
createPostgresAdapter. Reuses your existing Postgres instance with a small sidecar table. - Durable Object —
createDurableObjectAdapter. For Cloudflare Workers deployments where each DO instance acts as the sticky store for a shard of users. - Cookie / LocalStorage / IndexedDB — browser-side persistence adapters for single-page applications.
Per-SDK usage
1import { AvsbClient } from '@avsbhq/browser'2import { createLocalStorageAdapter } from '@avsbhq/utils/storage'3
4const client = new AvsbClient({5 sdkKey: 'sdk_production_abc123',6 storage: createLocalStorageAdapter({ namespace: 'avsb' }),7})8
9await client.onReady()10// Sticky assignments are now persisted across page reloads1import { AvsbServer } from '@avsbhq/node'2import { createRedisAdapter } from '@avsbhq/utils/storage'3import { createClient } from 'redis'4
5const redis = createClient({ url: process.env.REDIS_URL })6await redis.connect()7
8const server = new AvsbServer({9 sdkKey: process.env.AVSB_SDK_KEY,10 storage: createRedisAdapter({ client: redis, prefix: 'avsb:sticky:' }),11})12
13await server.onReady()1from avsb import AvsbServer2from avsb.storage import create_redis_adapter3import redis4
5r = redis.Redis.from_url(os.environ["REDIS_URL"])6
7server = AvsbServer(8 sdk_key=os.environ["AVSB_SDK_KEY"],9 storage=create_redis_adapter(client=r, prefix="avsb:sticky:"),10)11server.wait_for_ready()1import (2 "github.com/avsbhq/avsb-go"3 "github.com/avsbhq/avsb-go/storage"4 "github.com/redis/go-redis/v9"5)6
7rdb := redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDR")})8
9client, _ := avsb.NewServer(avsb.Options{10 SDKKey: os.Getenv("AVSB_SDK_KEY"),11 Storage: storage.NewRedisAdapter(rdb, storage.RedisOptions{Prefix: "avsb:sticky:"}),12})13client.WaitForReady(context.Background())DecideOption.IGNORE_STICKY_BUCKET in your decide options.