Why doesn't my rollout hit the percentage I set?

Last updated:

Bucketing assigns each user a fixed number from 0 to 99 and counts how many land under your threshold, rather than metering traffic. With a few hundred users the observed share can sit several points either side of the configured one, and that is expected. Persistent skew usually means contexts with no user key, which the rollout skips so they fall through to the next rule or the default.

You set a flag to 20% and your dashboard shows 26%. A percentage rollout counts users, never requests, so it behaves quite differently from a traffic meter.

Each evaluation hashes a per-flag salt together with the user’s key, takes the result modulo 100, and gets a number from 0 to 99. Variations are laid out in order with their weights accumulated, and the first band the number falls into wins. So a 20% rollout means “every user whose bucket is 0 through 19”, not “one request in five”.

Why the observed share drifts

The assignment is per user and permanent, so your observed percentage is a sample of however many distinct users happened to show up.

At a few hundred users, landing several points off the configured number is ordinary sampling noise. At tens of thousands it tightens considerably. Activity is uneven too, so one heavy user in the enabled bucket can drag a request-weighted figure around while the user-weighted figure sits exactly where it should. Count distinct users when you check.

When it is not noise

Some skew never settles down no matter how much traffic arrives, and contexts with no user key are the first thing to check. A rollout can only bucket a caller that has a stable key, so it skips the rest:

// Keyed: stable bucket, same variation every time
client.boolVariation('new-checkout', { user_id: 'u_8134' }, false);
// Keyless: no bucket, so the rollout is skipped
client.boolVariation('new-checkout', {}, false);

A keyless context never reaches the percentage split. It falls through to the next matching rule, or to the flag’s default variation when nothing else matches, on every call. That holds for the hosted endpoint and every server SDK, and the SDKs log a warning once per flag and rule. Your percentages still describe the callers who have a key, but they say nothing about the ones who don’t.

The other candidate is a rule sitting above the rollout. Rules run in priority order and the first match wins, so users captured by a fixed-serve rule never reach the fallthrough at all. If half your traffic matches an internal-staff rule, the rollout only ever divides the remaining half.

Checking the split in Featureflip

Each flag’s Analytics tab puts a rollout rule’s configured weights beside the share it actually served, with a sample-ratio check on top. Those numbers count evaluations, so a heavy user weighs more there as well. The check allows for that by warning only when the gap is both statistically significant and at least 2 points wide. A warning that persists usually means one variation’s code path reads the flag more often than the other’s, which inflates that variation’s count while the users themselves are split correctly.

The fix

Pass a stable identifier on every evaluation. If your users are genuinely anonymous, generate a key once, persist it in a cookie or local storage, and send it as the user_id. Those callers move from outside the rollout into real buckets off the back of that one change.

Then check what sits above the rollout in your rule list before trusting the number.

Rollout strategies has the hash and the weight bands in detail. For the rule-ordering half of the problem, start with Targeting and segments.

Still stuck?

The docs cover every SDK, and the Free plan is enough to reproduce most of these locally.