How Percentage Rollouts Actually Work: Deterministic Bucketing Explained
You set a flag to 10% and a user refreshes the page five times. Do they see the new feature once out of ten refreshes?
No — and if your flag system says otherwise, you have a bug, not a rollout. A percentage rollout isn't a dice roll on every request. It's a stable assignment: each user lands in a bucket once, and stays there.
The Problem With Random
Naive implementations call Math.random()at evaluation time. That gets the aggregate percentage right and everything else wrong. Users flip between old and new UI on every page load. Your A/B test data is garbage because nobody had a consistent experience. Support gets "the button keeps disappearing" tickets. And storing assignments in a database to fix it adds a lookup to your hottest code path.
The standard solution needs no storage at all: hash your way to consistency.
Hash, Mod 100, Compare
Featureflow's SDKs compute a bucket by hashing three values together — a salt, the feature key, and the user's bucket key — then taking the result mod 100:
import { createHash } from 'crypto';
/** Stable bucket in [0, 100) for a user + feature pair. */
function bucket(salt: string, featureKey: string, userKey: string): number {
const hash = createHash('sha1')
.update(`${salt}:${featureKey}:${userKey}`)
.digest('hex');
return parseInt(hash.substring(0, 15), 16) % 100;
}
// A 10% rollout serves the new variant when bucket < 10
const inRollout = bucket(salt, 'new-checkout', user.key) < 10;Three properties fall out of this, and they're what make percentage rollouts usable in production:
- Sticky. Same inputs, same bucket — on every request, every server, every SDK. No session state, no assignment table.
- Expandable. Moving from 10% to 50% widens the threshold. Everyone below 10 is still below 50, so early users keep the feature — nobody gets it taken away mid-rollout.
- Independent per flag. Because the feature key is part of the hash, a user in the first 10% of one flag isn't automatically in the first 10% of every flag. Without that, the same slice of your user base would absorb the risk of every rollout you run.
Choose Your Bucket Key Deliberately
The one real decision left is what identifier goes into the hash. A user ID keeps the experience consistent across a person's devices. An anonymous session key is fine for logged-out traffic but re-buckets people when the session rotates. An organisation ID rolls out to whole teams at once — usually what you want in B2B, where two colleagues comparing screens should see the same thing.
Whatever you pick, keep it stable. A bucket key that changes on every visit degrades your careful deterministic rollout right back into Math.random().
This is exactly how Featureflow evaluates percentage rollouts across all of its server and client SDKs — the same user gets the same variant whether the flag is evaluated in your Java backend or the browser. See the Java and JavaScript guides, and progressive rollout, for the evaluate call and the dashboard control.
#FeatureFlags#PercentageRollouts#ProgressiveDelivery#ContinuousDelivery#DevOps
Roll out to 1%, 10%, 100% — deterministically
Start free with Featureflow and get sticky percentage rollouts out of the box.
Start Now (Free)Related Articles
Retiring an API version without a surprise outage
A shutdown date hits every client at once, including the cron job nobody owns. Gate v1 with a per-client flag, brown out a slice of traffic, and turn a client back on if they still need the old routes.
Feature flags that only work when another flag is on
express-payments gets turned on for users who are still on the old checkout, and the page breaks while the dashboard looks healthy. Fold the two flags into one, or read both in a single function.
Five questions to ask when a pull request adds a feature flag
The flag is usually two lines inside a much bigger diff, so the review spends its time on the feature. Check the name, the failover, the tests, where it is evaluated, and who deletes it.