Do I need a user ID to evaluate a feature flag?

Last updated:

Not for on/off flags or targeting rules that match on other attributes. You need one for percentage rollouts, because the bucket comes from hashing the identifier with the flag key. Without it a server SDK evaluating locally serves the first (control) variation, while the hosted evaluation endpoint spreads anonymous traffic by weight with no per-user stickiness.

A flag that is simply on or off needs no context at all, and neither does a targeting rule keyed on something else, like a plan tier or a region. The identifier matters for one thing: deciding which side of a percentage split a given caller lands on.

Why the identifier is what makes a rollout stick

Bucketing hashes the user identifier together with the flag key, and the result is a number from 0 to 100. Same inputs, same number, every time. That is what stops a user flipping between variations on refresh, across devices, or between API calls.

Take the identifier away and there is nothing stable left to hash.

What actually happens without one

The two evaluation paths diverge here, and the difference catches people out.

A server SDK evaluating locally serves the first variation, the control. It does not randomize. An anonymous caller gets the safe side of the split deterministically, which is the conservative choice for a backend that may be handling a logged-out request.

The hosted evaluation endpoint spreads anonymous traffic across variations by weight. The overall split comes out roughly right, but no individual caller is sticky, so the same visitor can see different variations on consecutive requests.

Neither is a bug. They are different answers to the question of what to do when stickiness is impossible, chosen to suit where each one runs.

What to do for logged-out users

Generate a stable key yourself and pass it as the identifier. A UUID in a cookie or in local storage is enough:

let id = localStorage.getItem('ff-anon-id');
if (!id) {
id = crypto.randomUUID();
localStorage.setItem('ff-anon-id', id);
}
const client = FeatureflipClient.get({
clientKey: 'sdk_client_a1b2c3d4',
context: { user_id: id },
});

Now the anonymous visitor buckets like any other user and stays put. Swap the generated key for the real user ID once they sign in, and be aware that the bucket may change at that moment, because the hash input changed. For a checkout experiment that is usually fine. For anything where crossing the boundary mid-session would be jarring, hold the anonymous key until the session ends.

How much anonymity you keep is your call

Supplying a key buys stickiness back at any point, so the decision is about how much you want to track rather than about a limit in the flag system.

Bucketing mechanics, including the anonymous case, are documented in Rollout strategies.

Still stuck?

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