JavaScript web persistence and cookies - Docs - PostHog

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:

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:

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.

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:

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:

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.

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

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

// 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:

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