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 docs for rollout and targeting details.
#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
Feature Flags and SSR: Stop the Flash of the Wrong Variant
The server renders one variant, the browser boots without flag state and repaints another. Evaluate on the server, bootstrap the client with the same answers, and first paint stops lying.
Feature Flags in Serverless Functions: The Poll That Never Fires
Flag SDKs refresh config on a timer. A frozen container doesn't run timers, so it thaws holding hour-old config. Build the client at module scope, let evaluation drive the refresh, and pre-register failover variants for the cold path.
Feature Flags and the Login Boundary: Don't Let Sign-Up Reshuffle Your Variants
A visitor sees the new pricing page, signs up, and lands on the old one. Bucketing is a function of the user key — so carry the anonymous id across login, or your funnel experiments count one human as two.