Blog

Storing first-party identifiers in HttpOnly cookies

The cookie that JavaScript cannot read is the cookie that ad blockers cannot delete. Trade-offs and patterns for getting it right.

A cookie set by your tagging server with the HttpOnly flag is not visible to client-side JavaScript. That sounds limiting until you realise it is exactly what you want for first-party identity persistence: ad blockers and tracking-prevention extensions cannot delete it through the document.cookie API, and any leak from a malicious script on your page cannot exfiltrate it.

When HttpOnly makes sense

  • The user_pseudo_id you use to stitch sessions together server-side.
  • The Meta CAPI external_id that ties a CAPI event back to a user record.
  • Any first-party identifier you generate server-side and never need to read in the browser.

When it does not

If you need to read the cookie from JavaScript (most third-party analytics tools do), HttpOnly will silently break them. The classic example: GA4's _ga cookie cannot be HttpOnly because the gtag.js library reads it directly from document.cookie. Setting it to HttpOnly will not throw an error; it will just make the cookie invisible to the library.

Setting an HttpOnly cookie from sGTM

In a Custom Template, the setCookie API takes an options object:

setCookie('_user_id', userId, {
  domain: 'auto',
  path: '/',
  'max-age': 60 * 60 * 24 * 365,
  secure: true,
  httpOnly: true,
  sameSite: 'lax'
});

SameSite=Lax is the right default for analytics cookies. Strict will break cross-domain navigation in some flows; None requires Secure and increases your exposure to CSRF unless you have other protections.

The two-cookie pattern

A pattern we see in production: keep one HttpOnly cookie for the canonical server-side identity, and one regular cookie for the same value duplicated for client-side use. The client-side one is what JavaScript reads; the HttpOnly one is what the server trusts. If the client cookie is missing, your tagging server can rehydrate it from the HttpOnly one.

This costs you a little storage and some complexity, but it survives most aggressive privacy configurations that delete client-readable cookies on a schedule.

Lifetime considerations

Browsers cap first-party cookies set by JavaScript at 7 days (Safari ITP) or until the next storage cleanup (Firefox). Cookies set by the server via Set-Cookie headers are not subject to this cap and survive longer. Setting your identity cookies server-side gives you genuine multi-month persistence; setting them client-side gives you a week.