# JavaScript web persistence and cookies

#### Contents

- Synchronize identity and sessions across subdomains
- Cookie-persisted properties
- Example: tracking user interests across subdomains
- Persistence caveats

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`](https://developer.mozilla.org/en-US/docs/Web/API/Window/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.

```javascript
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`](/content/docs/libraries/js/usage#ensuring-flags-are-loaded-before-usage/index.html) 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.

```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`](/content/docs/libraries/js/usage#identifying-users/index.html) inside the iframe. Alternatively, [bootstrap PostHog with a distinct ID](/content/docs/libraries/bootstrapping#bootstrap-an-identity/index.html).

- 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](/content/docs/libraries/bootstrapping#bootstrap-an-identity/index.html) 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](/content/docs/advanced/browser-extension/index.html) for more details.
