JavaScript web persistence and cookies
Contents
For PostHog to work optimally, we store a small amount of information about the user on the user's browser. This ensures we identify users properly if they navigate away from your site and come back later.
The information we store includes:
- Their
distinct_id - Session ID & Device ID
- Active & enabled feature flags
- Any super properties you have defined
- Some PostHog configuration options (e.g. whether session recording is enabled)
By default, PostHog uses localStorage+cookie persistence. It stores the full state in localStorage and a smaller identity and session subset in a first-party cookie. This enables PostHog to identify visitors across sibling subdomains that can access the cookie. The cookie name is ph_<project_token>_posthog, and it expires after 365 days.
If you want to change how PostHog stores this information, you can do so with the persistence configuration option:
persistence: "localStorage+cookie"(default): Limited things are stored in the cookie such as the distinctID and the sessionID, and everything else in the browser'slocalStorage.persistence: "cookie": Stores all data in a cookie.persistence: "localStorage": Stores everything inlocalStorage.persistence: "sessionStorage": Stores everything insessionStorage.persistence: "memory": Stores everything in page memory, which means data is only persisted for the duration of the page view.
To change persistence values without reinitializing PostHog, you can use the posthog.set_config() method. This enables you to switch from memory to cookies to better comply with privacy regulations.
Synchronize identity and sessions across subdomains
With localStorage+cookie, localStorage belongs to one origin. When cross_subdomain_cookie is enabled, the first-party PostHog cookie is shared by sibling subdomains. This can cause a conflict when both stores contain the same key. For example:
- A tab on
www.example.comstores an anonymous identity in itslocalStorage. - Your app calls
posthog.identify()onapp.example.comand updates the shared cookie. - The first tab remains open, or the visitor returns to
www.example.com. ItslocalStoragestill contains the anonymous identity.
When cookieWinsOnConflict is disabled, the stale localStorage value wins this conflict. This is the default when defaults is unset. The tab can then capture events with the old identity or session. It can also write that old state back to the shared cookie.
Set cookieWinsOnConflict: true to make the shared cookie authoritative for the keys it contains:
This option requires posthog-js version 1.418.0 or later. It only applies to localStorage+cookie persistence. The SDK enables it by default when you set defaults: '2026-08-29' or a later defaults snapshot. It remains disabled when defaults is unset or earlier than 2026-08-29. An explicit cookieWinsOnConflict value overrides the snapshot default.
When enabled, PostHog synchronizes these cookie-backed values:
- Identity state, including the distinct ID, device ID, and anonymous or identified state
- Session state, including the session ID, last activity time, and session start time
- Session Replay sampling state, person-processing state, and initial-person information
- Properties that you add with
cookie_persisted_properties
At initialization, cookie values win matching values in that subdomain's localStorage. PostHog then updates localStorage with the synchronized state. In an open tab, PostHog checks for shared-cookie changes before captures and persistence writes. This means synchronization occurs on the tab's next PostHog activity, not immediately when another subdomain changes the cookie.
PostHog can adopt a shared identity during any of these checks. A persistence write can record the adoption without immediately reloading Feature Flags. When PostHog next processes the adopted identity, such as before a capture, it clears identity-bound Feature Flag state and starts a reload. onFeatureFlags callbacks run after the reload completes.
If the session ID changes, posthog.onSessionId() callbacks run when the tab next checks its session, such as during the next capture. A synchronized reset() also clears event and session properties that belonged to the previous identity.
This setting synchronizes state only between sibling subdomains that can access the same first-party cookie. It does not add tracking between unrelated domains or enable third-party cookies.
If you used the deprecated __preview_cookie_wins_on_conflict option, replace it with cookieWinsOnConflict.
Cookie-persisted properties
When using localStorage+cookie persistence (the default), most properties are stored in localStorage while only essential values like distinct_id and session ID go in the cookie. Since localStorage doesn't work across subdomains but cookies do, you can use the cookie_persisted_properties configuration option to specify additional properties that should be stored in the cross-subdomain cookie.
cookie_persisted_properties controls which additional properties PostHog shares in the cookie. It does not resolve conflicts between the cookie and localStorage. Use cookieWinsOnConflict for that conflict resolution.
This is useful when you need specific properties to be available across subdomains. For example, you might want to track which products a user has shown interest in on your marketing site and use that data to personalize their onboarding experience on your app subdomain.
You can then set these properties using posthog.register():
Example: tracking user interests across subdomains
Here's an example of tracking which products a user has viewed on a marketing site, then using that data for personalized onboarding on an app subdomain (like we do!):
Warning: Cookie size limits
Cookies have a maximum size of approximately 4KB. If your
cookie_persisted_propertiesstore large arrays or complex objects, you may exceed this limit, which can cause:
- Properties being silently truncated or not stored
431 Request Header Fields Too Largeerrors from your server- Unexpected behavior when reading properties
Keep cookie-persisted values small (short strings, small arrays of IDs). For larger data, consider using
localStoragepersistence and a different cross-subdomain strategy, or store the data server-side.
Persistence caveats
Be aware that
localStorageandsessionStoragecan't be used across subdomains. If you have multiple sites on the same domain, you may want to consider acookieoption or make sure to set all super properties across each subdomain.Due to the size limitation of cookies you may run into
431 Request Header Fields Too Largeerrors (e.g. if you have a lot of feature flags). In that case, uselocalStorage+cookie.Be careful when using cookie-based persistence inside iframes. If your app is embedded in an iframe on a different site, the browser treats those cookies as third-party cookies. Many browsers block or partition third-party cookies, which can prevent PostHog from reliably reusing the same
distinct_idor session across iframe loads. If you need stable identity in a cross-site iframe, pass an identifier from the parent page to the iframe and callposthog.identifyinside the iframe. Alternatively, bootstrap PostHog with a distinct ID.If you don't want PostHog to store anything on the user's browser (e.g. if you want to rely on your own identification mechanism only or want completely anonymous users), you can set
disable_persistence: truein PostHog's config. If your app knows the person's stable ID when PostHog initializes, bootstrap PostHog with that distinct ID instead of callingposthog.identify()after initialization. Without a stable bootstrapped ID, PostHog creates a new anonymous ID on every page load, and callingposthog.identify()merges each new ID into the identified person. If you want completely anonymous users, every page refresh is treated as a new and different user.For browser extensions, use
localStorage,sessionStorage, ormemory. Each extension context may initialize its own PostHog instance. These contexts don't share storage so the instances don't know about each other. Sincebrowser.storageandchrome.storageAPIs are not supported for data persistence, you'll need to provide your own shareddistinct_idduring each initialization to ensure events are sent under the same identifier. See the browser extension documentation for more details.