Device bucketing
Contents
Device bucketing is currently in beta. JavaScript web supports it automatically. Other SDKs support it when you send the device ID as $device_id in person properties when evaluating flags, including with local evaluation.
Device bucketing keeps feature flag values stable for anonymous and pre-login users. Instead of hashing on the user's distinct_id, PostHog hashes on $device_id, which stays the same when someone signs up, logs in, or logs out on the same browser or device.
Use it when a flag is shown before identify() runs, such as signup flows, landing page experiments, and onboarding that starts before authentication. If you need the same user to get the same value across multiple devices, keep the default user bucketing instead.
Why device bucketing exists
By default, release conditions match by user. That means PostHog uses distinct_id to decide which rollout bucket or variant someone gets.
That works well for identified users, but it can change during the transition from anonymous to identified:
| Step | distinct_id | $device_id | What happens |
|---|---|---|---|
| First anonymous visit | UUID-A | UUID-A | User and device bucketing return the same value |
After identify("user@example.com") | user@example.com | UUID-A | User bucketing may change, device bucketing stays stable |
After reset() | UUID-B | UUID-A | Device bucketing still uses the same device |
After reset(true) | UUID-C | UUID-C | PostHog treats it as a new device |
When you need consistency during login, device bucketing is the simpler choice than persisting feature flags across authentication steps. It avoids the tradeoffs of that approach, which disables local evaluation and bootstrapping, and adds extra server-side work.
When to use device bucketing
Use device bucketing when the flag targets users who aren't identified yet:
- Signup and onboarding flows: A user sees a new flow before creating an account, and should keep the same variant after signup.
- Pre-authentication pages: Landing pages, pricing pages, or any experience shown before the user is identified.
- Experiments on anonymous traffic: The experiment starts before login, but continues after login on the same browser or device.
- Hybrid anonymous and logged-in journeys: The same feature is visible both before and after authentication, and you want one stable experience on that device.
Don't use device bucketing when you need cross-device consistency for the same identified user. In that case, keep the default user bucketing.
User bucketing vs. device bucketing
| User bucketing | Device bucketing | |
|---|---|---|
| Hashes on | User's distinct_id | $device_id |
| Best for | Logged-in, identified users | Anonymous or pre-login experiences |
Behavior through identify() | Changes if distinct_id changes | Stays stable on the same device |
Behavior through reset() | Changes | Stays stable unless you call reset(true) |
| Behavior across devices | Stays stable for the same identified user | Changes (each device is evaluated independently) |
How to enable it
When creating or editing a feature flag, change the Match by dropdown under Release conditions from User to Device.
This is configured in PostHog, not in SDK code. Once the release condition matches by device, the SDK just needs access to the correct $device_id.
JavaScript web (automatic)
The browser SDK handles device bucketing automatically. On first load, it generates a device ID, stores it as $device_id, and includes it in flag evaluation requests.
If your backend also evaluates feature flags, send deviceId with the request so the server uses the same bucketing value as the browser.
Server-side SDKs
Server-side SDKs are stateless. They don't generate or persist device IDs for you. If you evaluate a device-bucketed flag on the server, pass the browser's device ID with the request and include it in the evaluation call.
In a full-stack app, the usual pattern is:
- The browser SDK creates and stores
$device_id. - Your frontend sends that value to your backend in a cookie, header, request body, or per-request context.
- Your backend passes it into the PostHog SDK whenever it evaluates a device-bucketed flag.
In the JavaScript web SDK, the anonymous distinct_id is usually the same value as $device_id. After identify(), keep sending the user's distinct_id and the unchanged $device_id.
In Python SDK v7.6.0 and above, you can pass device_id directly as shown above. The SDK also supports setting device_id once per request context and reusing it for all evaluations in that scope:
Shared devices and reset()
posthog.reset()keeps$device_id. Device-bucketed flags stay stable even after logout.posthog.reset(true)creates a new$device_id. Use this on shared devices or kiosks when each session should be treated as a new device.