Skip to main content

Inside the Cookie Jar, How Chrome Actually Handles Cookies (and Why It Matters for Developers)

· 9 min read
Pere Pages
Software Engineer

Cookies are tiny key–value blobs with rules. Chrome enforces those rules rigorously: where the cookie applies (domain/path), when it’s sent (request matching + SameSite), how it’s protected (Secure/HttpOnly), and how long it survives (expiry/eviction). This post walks through the full lifecycle, browser behavior, sharp edges, and production patterns—with TypeScript examples.

  • Use server-set, Secure; HttpOnly; SameSite=Lax cookies for auth.
  • Scope deliberately with domain and path; avoid accidental overlaps.
  • Understand SameSite (and partitioning) for cross-site flows.
  • Keep cookies small, few, and purposeful.

1) The lifecycle at a glance

  1. Creation

    • Server → Browser via Set-Cookie (preferred for auth/security).
    • JS → Browser via document.cookie = "name=value; ..." (UI/prefs).
  2. Storage

    • Chrome persists cookies per profile; session cookies live in memory.
  3. Attachment

    • On each request, Chrome evaluates domain, path, Secure, expiry, and SameSite to decide which cookies to send.
  4. Access

    • Server sees them in the Cookie: request header.
    • JS can only see non-HttpOnly cookies scoped to the current document’s origin/path.
    • Extensions (with permission) can read/manage via chrome.cookies.*.

2) Scope: domain and path (the “where”)

Domain

  • example.com (hostOnly): sent only to that exact host.
  • .example.com: sent to example.com and all subdomains (api.example.com, app.example.com).
  • Choose .example.com to share sessions across subdomains; choose hostOnly to isolate surfaces (e.g., keep admin.example.com separate from www.example.com).

Path

  • Prefix match. path=/account matches /account, /account/, /account/settings but not /.
  • Defaults to the current directory if omitted when set from JS (/shop/cart → default path /shop/).
  • Use path scoping to segment areas like /admin or /v2.

Coexistence & precedence

  • You can have multiple cookies with the same name if they differ by path and/or domain. Browsers send the ones that match; RFC ordering prefers longest path first.
  • Overwrites only happen when name + domain + path are the same.

3) Security model: Secure, HttpOnly, SameSite

Secure

  • Sent only over HTTPS. Required for any sensitive data. Also required when SameSite=None.

HttpOnly

  • Hidden from document.cookie. Crucial for mitigating XSS on session cookies.

SameSite (schemeful, i.e., scheme is considered in site calculation)

  • Strict: not sent on cross-site navigations at all (strong CSRF protection, can break some flows).
  • Lax (modern default if unspecified): sent on top-level navigations with safe methods (e.g., clicking a link), but not in iframes or subresource requests.
  • None: always sent cross-site, must be Secure.

Modern baseline for sessions

Set-Cookie: session=...; Path=/; Secure; HttpOnly; SameSite=Lax

Use SameSite=None only when you must support third-party contexts (embedded widgets, federated login on a different site), and accept the extra CSRF surface with compensations (double-submit tokens, SameSite-sensitive flows).


4) Expiration, session, and eviction

  • Session cookie: no Expires/Max-Age → removed when the browser closes.
  • Persistent: specify Expires=<date> or Max-Age=<seconds>.
  • Eviction & limits: browsers cap total cookies, cookies per eTLD+1, and per-cookie size (~4 KB). If you push limits, Chrome will evict older/least recently used cookies. Keep cookies small and few.

5) How Chrome decides which cookies to send

For each outgoing request:

  1. Filter by domain (hostOnly vs parent-match) and path (prefix).
  2. Enforce Secure → HTTPS only.
  3. Check not expired.
  4. Apply SameSite based on first/third-party context.
  5. Potentially apply partitioning (more below).
  6. Order cookies (e.g., by longest path) and serialize Cookie: name=value; ....

6) Modern Chrome features to know

  • __Host-:

    • Must be set with Secure, no Domain attribute (hostOnly), Path=/.
    • Great for strong, host-bound session cookies.
  • __Secure-:

    • Must be set with Secure. Domain allowed.

These are enforced by browsers—cheap guardrails that catch config mistakes.

Partitioned cookies (CHIPS)

  • A cookie can be partitioned so that if it’s set in a third-party context, it’s siloed per top-level site. This mitigates cross-site tracking while still enabling embedded functionality.
  • Server attribute: Partitioned (and usually Secure; SameSite=None in 3P use).
  • Chrome increasingly restricts third-party cookies. Expect to rely on SameSite, partitioning, and/or backend patterns (token relay/redirects) to make embedded flows work.

7) JS vs Server: when to set cookies where

Server-set (preferred for auth)

  • You can mark HttpOnly, Secure, SameSite, control domain/path precisely, and avoid exposing tokens to JS.
  • Use for sessions, refresh tokens, CSRF tokens, and anything sensitive.

JS-set (UI concerns)

  • document.cookie can’t set HttpOnly. It’s fine for non-sensitive data:

    • theme=dark, lang=ca, “dismissed the banner”, A/B bucket, etc.
  • Keep them short-lived and scoped.


8) Practical examples

Set-Cookie: session=eyJhbGci...; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=86400

B. Cross-site embedded widget needs session

Set-Cookie: widget_sess=...; Path=/; Secure; HttpOnly; SameSite=None; Partitioned; Max-Age=1800

(Expect to harden CSRF with additional measures.)

C. Host-bound session with guardrails

Set-Cookie: __Host-session=...; Path=/; Secure; HttpOnly; SameSite=Lax

No Domain → cannot bleed to subdomains.

D. JS preferences (not sensitive)

document.cookie = [
"theme=dark",
"Path=/",
"Max-Age=31536000",
"SameSite=Lax"
].join("; ");

9) Multiple cookies with the same name (real-world behavior)

You might see:

Set-Cookie: token=AAA; Path=/; Domain=.example.com
Set-Cookie: token=BBB; Path=/admin; Domain=.example.com

Requests to /admin/* can carry both. Ordering favors the longer path first:

Cookie: token=BBB; token=AAA

On the server, don’t assume one value—parse carefully and define a canonical scope for critical cookies (prefer a unique name or a path that isolates the one you actually read).


10) Testing & debugging

  • DevTools → Application → Storage → Cookies: inspect, edit, delete.

  • Network tab: check Set-Cookie responses and Cookie requests.

  • curl:

    curl -I https://app.example.com \
    -H 'Cookie: theme=dark; exp-group=B'
  • MitM / proxy tools can help verify attachment and SameSite behavior during cross-site navigations or iframe loads.


11) Common pitfalls (and fixes)

  • Missing Secure on production → cookies ride over HTTP on misconfig or local proxies. Fix: always set Secure in prod; use HTTPS everywhere.
  • Forgot HttpOnly on sessions → XSS can steal tokens. Fix: mark sessions HttpOnly and keep auth out of JS.
  • SameSite confusion → cross-site flows fail silently. Fix: understand when you truly need None (and then add Secure), or prefer Lax/Strict.
  • Overwriting didn’t work → domain/path mismatch created a parallel cookie. Fix: delete with the same triplet (name+domain+path) and re-set consistently.
  • Cookies too big → eviction or truncated headers. Fix: keep under ~4 KB each; store bulky stuff server-side.
  • Subdomain leakage → sensitive cookie available on *.example.com. Fix: use __Host- cookies or hostOnly domain.

12) TypeScript helpers you can drop in

// Parse document.cookie into a map (non-HttpOnly only)
export function readCookie(name: string): string | undefined {
const target = name + "=";
const parts = document.cookie.split("; ");
for (const p of parts) {
if (p.startsWith(target)) return decodeURIComponent(p.slice(target.length));
}
return undefined;
}

export function setClientCookie(
name: string,
value: string,
opts: {
path?: string;
maxAge?: number; // seconds
samesite?: "Lax" | "Strict" | "None";
secure?: boolean;
domain?: string; // avoid for host-bound prefs
} = {}
) {
const segs = [
`${name}=${encodeURIComponent(value)}`,
`Path=${opts.path ?? "/"}`
];
if (opts.maxAge !== undefined) segs.push(`Max-Age=${opts.maxAge}`);
if (opts.domain) segs.push(`Domain=${opts.domain}`);
if (opts.secure) segs.push(`Secure`);
if (opts.samesite) segs.push(`SameSite=${opts.samesite}`);
document.cookie = segs.join("; ");
}

export function deleteCookie(
name: string,
{ path = "/", domain }: { path?: string; domain?: string } = {}
) {
const segs = [
`${name}=`,
`Path=${path}`,
`Expires=Thu, 01 Jan 1970 00:00:00 GMT`
];
if (domain) segs.push(`Domain=${domain}`);
document.cookie = segs.join("; ");
}

Chrome extension (permissions granted):

chrome.cookies.getAll({ domain: ".example.com" }, cookies => {
cookies.forEach(c => console.log(c.name, c.value, c.path, c.sameSite));
});

chrome.cookies.set({
url: "https://app.example.com/",
name: "feature-flag",
value: "checkout-v2",
secure: true,
sameSite: "lax",
path: "/",
domain: "app.example.com"
});

13) Design patterns that age well

  • Keep tokens server-side; cookies carry only opaque IDs.
  • One cookie, one purpose. Avoid multiplexing JSON blobs.
  • Use __Host- for primary session; use separate names for per-section signals.
  • Prefer SameSite=Lax unless you truly need cross-site; for cross-site, combine SameSite=None; Secure with CSRF defenses.
  • Partition third-party cookies when embedding.
  • Instrument and log cookie attributes server-side (especially during migrations or library changes).

1️⃣ Auth flows (SPA + API)

Goals: secure, reliable, CSRF-resistant sessions.

Server cookie config

Set-Cookie: __Host-session=<opaque_id>;
Path=/;
Secure;
HttpOnly;
SameSite=Lax;
Max-Age=86400

__Host- prevents subdomain leakage. ✅ Secure; HttpOnly; SameSite=Lax is modern safe default. ✅ Store opaque session ID, not JWTs or user data. ✅ Rotate cookies on login/logout. ✅ Clear on logout using same name+path+domain triple.

Frontend checklist

  • Never read/write session cookies via JS.
  • Use fetch with credentials: "include" or axios { withCredentials: true }.
  • CSRF defense: rely on SameSite=Lax + server-generated anti-CSRF tokens for state-changing requests.

2️⃣ Embedded widgets / third-party contexts

Goals: allow cookie-based sessions inside iframes or external embeds safely.

Server cookie config

Set-Cookie: widget_session=<opaque_id>;
Path=/;
Secure;
HttpOnly;
SameSite=None;
Partitioned;
Max-Age=1800

SameSite=None → allows cross-site requests. ✅ Partitioned → isolates per top-level site (CHIPS). ✅ Still requires HTTPS (Secure). ✅ Keep lifetime short (~30 min).

Frontend checklist

  • Only use this when embedding under another domain.
  • Consider token relay or postMessage API for long-term sessions.
  • Log SameSite and Partitioned behavior during QA in Chrome DevTools → Network → Cookies.

3️⃣ Local development

Goals: mimic prod behavior safely.

✅ Use localhost + HTTPS via mkcert or self-signed cert. ✅ In .env.local:

COOKIE_DOMAIN=localhost
COOKIE_SECURE=false
COOKIE_SAMESITE=Lax

✅ Avoid .local or 127.0.0.1 subdomain experiments—Chrome treats them differently. ✅ For debugging, inspect in DevTools → Application → Cookies → http://localhost.


4️⃣ End-to-end tests (Cypress, Playwright, etc.)

Goals: simulate sessions easily without server logins.

✅ Use API route to log in programmatically and retrieve cookie:

const cookies = await api.post("/login", { user, pass });
page.context().addCookies(cookies);

✅ Or mock cookie in test runner:

cy.setCookie('__Host-session', 'test-session', {
domain: 'localhost',
path: '/',
secure: false,
sameSite: 'Lax',
});

✅ Always test both authenticated and unauthenticated flows. ✅ Clean cookies between tests with cy.clearCookies() or Playwright context.clearCookies().


🧪 Quick Recipes

  1. Open Application → Storage → Cookies → [site].
  2. Verify flags: Secure, HttpOnly, SameSite.
  3. Check the Expires/Max-Age column for persistence.

🧰 curl examples

Inspect what cookies server sets

curl -I https://app.example.com/login
# Look for "Set-Cookie" headers

Send a manual request with a cookie

curl -v https://api.example.com/user \
-H "Cookie: __Host-session=abc123"

Delete a cookie

curl -X POST https://app.example.com/logout
# or explicitly set expired cookie header

🧭 Local debug tip

When testing SameSite/Partitioned behavior:

chrome://flags/#cookies-without-same-site-must-be-secure
chrome://flags/#third-party-storage-partitioning

→ enable both for production-like cookie enforcement.