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
-
Creation
- Server → Browser via
Set-Cookie
(preferred for auth/security). - JS → Browser via
document.cookie = "name=value; ..."
(UI/prefs).
- Server → Browser via
-
Storage
- Chrome persists cookies per profile; session cookies live in memory.
-
Attachment
- On each request, Chrome evaluates domain, path, Secure, expiry, and SameSite to decide which cookies to send.
-
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.*
.
- Server sees them in the
2) Scope: domain
and path
(the “where”)
Domain
example.com
(hostOnly): sent only to that exact host..example.com
: sent toexample.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., keepadmin.example.com
separate fromwww.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 beSecure
.
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>
orMax-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:
- Filter by domain (hostOnly vs parent-match) and path (prefix).
- Enforce Secure → HTTPS only.
- Check not expired.
- Apply SameSite based on first/third-party context.
- Potentially apply partitioning (more below).
- Order cookies (e.g., by longest path) and serialize
Cookie: name=value; ...
.
6) Modern Chrome features to know
Cookie prefixes
-
__Host-
:- Must be set with
Secure
, noDomain
attribute (hostOnly),Path=/
. - Great for strong, host-bound session cookies.
- Must be set with
-
__Secure-
:- Must be set with
Secure
. Domain allowed.
- Must be set with
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 usuallySecure; SameSite=None
in 3P use).
Third-party cookie restrictions
- 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 setHttpOnly
. 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
A. Minimal, safe auth cookie (server)
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 andCookie
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 setSecure
in prod; use HTTPS everywhere. - Forgot
HttpOnly
on sessions → XSS can steal tokens. Fix: mark sessionsHttpOnly
and keep auth out of JS. - SameSite confusion → cross-site flows fail silently.
Fix: understand when you truly need
None
(and then addSecure
), or preferLax/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, combineSameSite=None; Secure
with CSRF defenses. - Partition third-party cookies when embedding.
- Instrument and log cookie attributes server-side (especially during migrations or library changes).
14) ✅ Cookie Checklist for Dev Teams
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
withcredentials: "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
andPartitioned
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
🔍 Inspect cookie details in Chrome DevTools
- Open Application → Storage → Cookies → [site].
- Verify flags:
Secure
,HttpOnly
,SameSite
. - 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.