Percentage rollouts

Deterministic, cross-language rollout bucketing.

A percentage rollout enables a flag for a deterministic fraction of your users — 10% first, then 25%, then everyone — without code changes. It's how you ramp a feature safely.

Setting a rollout

Each flag-environment has a rollout_pct. Think of it as the fallthrough: when the flag is enabled and a user matches no targeting rule, the rollout decides whether they get the enabled value.

  • rollout_pct = 25 → roughly 25% of users get the flag.
  • rollout_pct = 0 → nobody gets it via the rollout (rule matches still do).
  • rollout_pct = 100everyone gets it.
  • unset (no rollout configured) → the default for a new flag — see the note below for how it resolves.

Deterministic bucketing

A user is bucketed by hashing their user_id together with the flag key:

bucket = sha256("{user_id}:{flag_key}") % 100
user is in the rollout if bucket < rollout_pct

Two consequences fall out of this:

  • Stable per user. The same user always lands in the same bucket for a given flag, so they don't flicker between on and off across requests or page loads. User "42" either always sees the feature at 25% or never does.
  • Stable as you ramp. Because the bucket doesn't depend on the percentage, everyone in the 10% cohort is still included when you go to 25% — you only ever add users as you increase the percentage, never reshuffle them.

A user_id (or id) is required for rollouts. With no user context, a flag at less than 100% can't bucket the caller, so it falls back to the default value.

Identical across languages

The hashing is part of the cross-language SDK contract: Python and JavaScript compute the same bucket for the same user_id:flag_key. A user bucketed into the 25% cohort gets the feature whether your backend (Python) or your frontend (JavaScript) does the check. This parity is pinned by a shared set of test vectors that both SDKs run.

The "no rollout configured" default

A new flag-environment starts with no rollout configured, and what that means depends on whether you've added targeting rules:

  • No rules, no rollout → the flag is a plain on/off switch: enabled serves everyone, disabled serves no one.
  • Rules, no rollout → only the users who match a rule get the flag; the fallthrough is effectively 0%. Adding a rule narrows the audience to exactly that rule — it doesn't leak to everyone else.
  • Rules + a rollout → rule matchers always get it, plus the rollout percentage of everyone else.

Internally "no rollout configured" is stored as a null rollout_pct. With no rules it resolves to 100 (everyone); with rules present the backend publishes it as a 0% fallthrough so the rules actually scope the audience. The dashboard shows the rollout slider once you deliberately set a percentage — and you can set it to a full 100% (an explicit "serve everyone," distinct from leaving it unset). (See ADR-008 for the reasoning and the resolution rule — the SDKs read the resolved number, so the evaluator itself stays a simple bucket < rollout_pct check.)

Next