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's localStorage.

  • persistence: "cookie" : Stores all data in a cookie.

  • persistence: "localStorage": Stores everything in localStorage.

  • persistence: "sessionStorage": Stores everything in sessionStorage.

  • 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.

Web
const handleCookieConsent = (consent) => {
posthog.set_config({ persistence: consent === 'yes' ? 'localStorage+cookie' : 'memory' });
localStorage.setItem('cookie_consent', consent);
};

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:

  1. A tab on www.example.com stores an anonymous identity in its localStorage.
  2. Your app calls posthog.identify() on app.example.com and updates the shared cookie.
  3. The first tab remains open, or the visitor returns to www.example.com. Its localStorage still 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:

JavaScript
posthog.init("<ph_project_token>", {
api_host: "https://us.i.posthog.com",
persistence: "localStorage+cookie",
cross_subdomain_cookie: true,
cookieWinsOnConflict: true,
})

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.

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.

JavaScript
posthog.init('<ph_project_token>', {
api_host: 'https://us.i.posthog.com',
persistence: 'localStorage+cookie',
cookie_persisted_properties: ['user_preferences', 'signup_source'],
})

You can then set these properties using posthog.register():

JavaScript
// Store a property that will persist in the cookie
posthog.register({ signup_source: 'product-page' })
// Later, read it back (works across subdomains)
const source = posthog.get_property('signup_source')

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!):

JavaScript
// On marketing site (e.g., example.com)
posthog.init('<ph_project_token>', {
api_host: 'https://us.i.posthog.com',
persistence: 'localStorage+cookie',
cookie_persisted_properties: ['product_interests'],
})
// When user visits a product page
function trackProductInterest(productSlug) {
const currentInterests = posthog.get_property('product_interests') || []
if (!currentInterests.includes(productSlug)) {
currentInterests.push(productSlug)
}
posthog.register({ product_interests: currentInterests })
}
// On app subdomain (e.g., app.example.com)
// The property is automatically available because it's in the cross-subdomain cookie
const interests = posthog.get_property('product_interests') || []
// interests = ["analytics", "session-replay", ...]

Warning: Cookie size limits

Cookies have a maximum size of approximately 4KB. If your cookie_persisted_properties store 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 Large errors from your server
  • Unexpected behavior when reading properties

Keep cookie-persisted values small (short strings, small arrays of IDs). For larger data, consider using localStorage persistence and a different cross-subdomain strategy, or store the data server-side.

Persistence caveats

  • Be aware that localStorage and sessionStorage can't be used across subdomains. If you have multiple sites on the same domain, you may want to consider a cookie option 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 Large errors (e.g. if you have a lot of feature flags). In that case, use localStorage+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_id or 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 call posthog.identify inside 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: true in PostHog's config. If your app knows the person's stable ID when PostHog initializes, bootstrap PostHog with that distinct ID instead of calling posthog.identify() after initialization. Without a stable bootstrapped ID, PostHog creates a new anonymous ID on every page load, and calling posthog.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, or memory. Each extension context may initialize its own PostHog instance. These contexts don't share storage so the instances don't know about each other. Since browser.storage and chrome.storage APIs are not supported for data persistence, you'll need to provide your own shared distinct_id during each initialization to ensure events are sent under the same identifier. See the browser extension documentation for more details.

Still have questions?

Was this page useful?