Keeping flag evaluations stable
Contents
Feature flags are pure functions – same inputs, same outputs. The most common way to break that contract is to change the distinct_id between evaluations, which happens naturally when a user transitions from anonymous to identified.
This page covers how to keep the hash input stable so flags don't flip during authentication transitions. If you haven't already, read identity resolution first – a stable identity strategy eliminates most flag stability problems before they start.
Why the hash input changes
When a user starts anonymous and then logs in:
Same person, different variant. The hash function worked correctly both times – it received different inputs and produced different outputs.
For experiments, this is data corruption: the user experienced "test" but may now be counted in "control." For feature rollouts, the user sees a feature appear and then disappear on login.
Start with your identity strategy
The best way to keep flag evaluations stable is to prevent the distinct_id from changing in the first place. The identity resolution guide covers three approaches:
- Golden path: stable ID from first touch – assign a durable ID before any flag evaluation happens. The
distinct_idnever changes, so the hash never changes. This is the recommended approach. - Common path: anonymous then identify on login – the SDK generates an anonymous ID, then
identify()links it to a known user. This changes thedistinct_id, which changes the hash. The mitigations below exist for this path. - Server-side bootstrap with a known ID – if you have server rendering, you know the user before the page loads. Bootstrap with their stable ID and the anonymous-to-identified transition never happens.
If you can use the golden path or server-side bootstrap, the rest of this page is unnecessary. The sections below are for when the distinct_id will change and you need to manage that.
Flag-specific mitigations
When your identity strategy involves a distinct_id change (the common path), PostHog offers two mechanisms to keep flag values stable across that transition.
Device bucketing (recommended)
Hashes on $device_id instead of distinct_id. Since the device ID doesn't change on identify(), the flag value stays consistent across the authentication boundary.
See Device bucketing for setup.
| Device bucketing | Experience continuity | |
|---|---|---|
| Database cost | None | Read on every evaluation, write on first identify |
| Local evaluation | Supported | Not supported – forces network round-trip |
| Bootstrapping | Compatible | Not compatible |
person_profiles: 'identified_only' | Works | Doesn't work |
| Cross-device consistency | No (per-device) | No (per-person, but only after identify) |
| Known issues | $device_id can be lost on cookie clears | posthog-js #2623: values can change after identify |
Experience continuity
Pins a stable hash key to a person record in Postgres. Enabled per-flag via ensure_experience_continuity. The server remembers which hash key was used for the first evaluation and reuses it on all subsequent evaluations, even after the distinct_id changes.
See Creating feature flags – persisting across authentication for setup.
Use experience continuity only when device bucketing and the stable ID approaches aren't feasible. It comes at a cost: a database read and write on every evaluation, no support for local evaluation, and a known issue where values can still change after identify().
Cross-device consistency
Device-scoped solutions (device bucketing, cookie-based stable IDs) don't span devices. If a user logs in on their phone and laptop, each device hashes independently.
For cross-device consistency, target flags on person properties instead of relying on the hash:
Person properties resolve server-side against the person record. The same person matches the same conditions regardless of device. This reframes the question: instead of "how do I make the hash stable across devices," ask "how do I target users based on who they are?"
Choosing the right pattern
| Requirement | Best approach |
|---|---|
| Same flag value before and after login (single device) | Stable ID or device bucketing |
| Same flag value across devices (logged-in users) | Target on person properties |
| Same flag value across devices (anonymous users) | Not solvable without cross-device identity |
| Maximum debuggability and control | Server-side local eval with explicit stable ID |
| Quick fix, minimal code changes | Device bucketing |
| Can't change identity flow at all | Experience continuity |
Further reading
- Production-ready feature flags – the pure function model that explains why identity matters for flags
- Identity resolution – designing your identity strategy (start here if you haven't)
- Device bucketing – setup guide
- Bootstrapping – eliminating the async gap
- Local evaluation – server-side evaluation for explicit input control