← Academy index
Chapter 3 · 10 min read

Targeting and Rollouts

Rules, segments, percentage rollouts, and the sticky-bucketing trick that keeps the same user in the same variant, every time, on every device, forever.

targetingrolloutssegments

A feature flag without targeting is just a boolean in a database. The reason flag systems exist is to answer the question “should this user, in this context, right now, see the new thing?”, and to answer it deterministically, fast, and the same way across every server in your fleet.

This chapter is how that machine works.

The rule, conceptually

Every flag evaluation is a small pipeline:

  1. Match context. Take the user attributes (id, plan, country, beta, whatever) and walk the flag’s rules top-to-bottom. The first matching rule wins.
  2. Resolve the variant. A matching rule either returns a fixed value (on, off, "variantA", a JSON blob) or hands off to a rollout that picks a value based on the user.
  3. Fall through to the default. No rules matched? Return the flag’s default value.

That’s it. The complexity hides inside step 2.

Rule 1: if plan == "enterprise"        →  on
Rule 2: if country in [US, CA]          →  50% on, 50% off
Rule 3: if beta_opt_in == true          →  on
Default:                                →  off

A user in Spain with beta_opt_in=true gets on. A US Pro user gets a 50/50 coin flip, but the same coin flip every time. That’s the trick worth understanding.

Sticky bucketing: the same flip, every time

A percentage rollout is not random. If it were random, the same user would see the new checkout on Monday and the old one on Tuesday, a UX disaster and a measurement nightmare.

The mechanism is deterministic hashing:

bucket = hash(flag_id + ":" + user_id) mod 10000

That gives you a stable number between 0 and 9999 for every (flag, user) pair. A 50% rollout means: include users whose bucket is < 5000. A 1% rollout: bucket < 100. Increase the rollout to 60% and only new users (buckets 5000–5999) start seeing the new variant, nobody flips back.

This single property, monotonic inclusion, is why percentage rollouts work as a delivery strategy.

The hashing function has to be the same across SDKs. Server SDK and client SDK must compute the same bucket for the same user; otherwise you get split-brain. (Most flag systems use a 32-bit MurmurHash or a similar fast non-cryptographic hash. ShipSilently uses xxhash64.)

Segments: rules that are reusable

A segment is a named bag of targeting rules: power-users, eu-customers, q3-beta. Flags reference segments rather than re-stating the rules:

Rule 1: if user in segment "power-users"  →  on

Segments are how you stop copy-pasting the same plan == "enterprise" OR mrr > 5000 OR … filter across thirty flags. They also give the marketing team a place to define cohorts without learning the rule editor.

Two segments worth having from day one:

  • internal, everyone with an @yourcompany.com email. Lets you turn on every new feature for yourselves first.
  • beta, opted-in users. The pre-flight check before percentage rollouts.

Identifying users

Targeting only works if you give it a stable user ID. The two worst patterns:

  • Email as ID. Works until someone changes their email. Now they’re a new user as far as bucketing is concerned. Sticky-no-longer.
  • Session ID as ID. Every visit is a new “user”, your 50% rollout becomes a 50% coin flip on every page load. Welcome back to the random-on-every-request hellworld.

The right answer is a durable user ID (usually your database primary key, or an anonymous device ID for logged-out traffic), passed in identically on every SDK call. Pre-login, use the device ID. After login, use the user ID. The flag system should let you alias the two so identity carries forward across the login event.

flags.identify({ id: "user_47a92", deviceId: "anon_…" });

Progressive rollouts in practice

The maturity ladder for rolling out a single feature, in increasing order of confidence:

  1. Off in prod, on locally. You’re still building.
  2. On for internal segment. Dogfood it.
  3. On for beta segment. Trusted external users.
  4. 1% of all users. You think it’s safe. Reality has a vote.
  5. 5%, 25%, 50%, 100%. Each step held for long enough that any latent issue would surface, usually 24–48h for a consumer app, longer for B2B with weekly usage rhythms.
  6. Delete the flag. Yes, delete it. Chapter 6.

Step 4 is where the actual risk reveals itself. The bug that didn’t show up locally, in staging, or with internal users will show up at 1% in prod. That’s not a failure, that’s the system working. The right move when 1% breaks is to flip back to 0% and investigate. The wrong move is to push through.

The full playbook lives in the progressive rollouts guide.

Targeting attributes that earn their keep

The right set of targeting attributes is the smallest set that lets you express any rollout you’d realistically want. Most teams need:

  • id (user ID, required)
  • email
  • plan (free / pro / enterprise)
  • country (ISO code)
  • org_id or account_id
  • created_at (lets you target “users who signed up after X”)
  • device (web / ios / android)
  • app_version (mobile-critical, and exactly why the mobile feature flag problem is its own animal)

Resist adding more. Every attribute is one more thing your SDK has to serialize on every call, one more thing your marketing team has to remember exists, and one more vector for “wait, why is this flag on for me?” debugging sessions.

The two failure modes

Targeting goes wrong in two ways:

  1. Rule order bugs. A broader rule earlier in the list eats a narrower rule below it. Most flag systems evaluate top-to-bottom; if you put country == US → off above beta_opt_in == true → on, your US beta testers don’t see the feature. The lint rule is: most-specific rule first, most-general last.

  2. Drift between SDKs. Server thinks the user is in variant A, client thinks variant B. Almost always caused by passing different attributes (server has country, client doesn’t) or by evaluating on the client before attributes are loaded. Fix: evaluate once on the server and pass the decision through the response.

If you’re seeing weird, transient targeting bugs, it’s almost always one of those two.


Previous: Chapter 2, Architecture: where flags live  ·  Next: Chapter 4, Kill switches and incident response