Cookies, sessions and state
Set-Cookie attributes, Domain, Path, Secure, HttpOnly and SameSite, session identifiers, third-party cookie restrictions, and storage alternatives.
Set-Cookie and its attributes
HTTP is stateless: each request stands alone. Cookies are the mechanism that lets a server recognise the same client across requests, and every protection you get comes from the attributes you attach when you set them.
HTTP/1.1 200 OK
Set-Cookie: sid=8f3c1a...; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=604800
Set-Cookie: theme=dark; Path=/; Max-Age=31536000; SameSite=Lax| Attribute | What it does | Leave it out and |
|---|---|---|
Domain | Widens the cookie to a domain and its subdomains | The cookie is host-only, which is usually what you want |
Path | Limits which paths the browser sends it to | It defaults to the directory of the request |
Secure | Send only over HTTPS | The cookie travels in plaintext on the first HTTP request |
HttpOnly | Hidden from JavaScript | Any XSS can read the session |
SameSite | Lax, Strict or None cross-site behaviour | Browsers treat it as Lax |
Max-Age / Expires | Lifetime | It is a session cookie, gone when the browser closes |
Partitioned | Scopes the cookie to the embedding site | Third-party cookies are blocked outright |
- A cookie is scoped by host and path, never by port. Two apps on ports 8080 and 8081 of one host share cookies.
- Cookies are not secret storage: they sit in the browser profile and are attached automatically to matching requests.
- Deleting a cookie means sending
Set-Cookiewith the same name, path and domain plusMax-Age=0.
Session identifiers
GET /dashboard HTTP/1.1
Host: app.example.com
Cookie: sid=8f3c1a...; theme=dark
If-None-Match: "v7"// Node/Express sketch: server-side session in Redis
app.use(session({
name: 'sid',
secret: process.env.SESSION_SECRET,
store: new RedisStore({ client: redis }),
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 1000 * 60 * 60 * 8
}
}));- The cookie carries only an opaque identifier; the data lives server-side, so you can revoke a session by deleting the record.
- Rotate the identifier on login and on privilege change to prevent session fixation, where an attacker plants a known id beforehand.
- Enforce both an idle timeout and an absolute one: eight hours of inactivity, or seven days regardless of activity.
- Logging out must invalidate the server-side record, not merely clear the cookie — a copied cookie would otherwise still work.
⚠️
SameSite=Lax is the browser default and stops most cross-site POSTs, but it is not a CSRF defence on its own. Keep the token-based protection as well, and add Vary: Cookie or a private cache directive so a shared cache can never serve one user's page to another.Third-party restrictions and alternatives
| Store | Sent automatically? | Accessible to JS? | Survives |
|---|---|---|---|
| Cookie | Yes, on matching requests | No, if HttpOnly | Its expiry, across tabs |
localStorage | No | Yes | Until cleared |
sessionStorage | No | Yes | Until the tab closes |
| IndexedDB | No | Yes | Until cleared; suited to larger data |
| In-memory | No | Yes | Until reload |
// an in-memory access token avoids XSS readability but dies on reload
let accessToken = null;
export function setToken(t) { accessToken = t; }
export function authHeader() {
return accessToken ? { Authorization: 'Bearer ' + accessToken } : {};
}
// and refresh through an HttpOnly cookie the JS never sees
async function refresh() {
const r = await fetch('/auth/refresh', { method: 'POST', credentials: 'include' });
if (!r.ok) throw new Error('session expired');
setToken((await r.json()).access_token);
}- Third-party cookie blocking breaks embedded widgets that relied on them; same-origin iframes and
Partitionedcookies are the replacement patterns. - Storing credentials in
localStorageturns any XSS into a lasting account takeover — anHttpOnlycookie is strictly better for sessions. - A token in memory plus an
HttpOnlyrefresh cookie is the common compromise for single-page apps.
FAQ
Do cookies affect caching?
They can. A response that varies by cookie must not be stored in a shared cache; mark it
Cache-Control: private or send Vary: Cookie. Otherwise one user's authenticated page may be served to another.What size can a cookie be?
About 4 KB per cookie, and browsers limit the number per domain to roughly 50, with older limits on the total. Keep identifiers small and put bulk data in server-side storage.
Related
Authentication on the wire HTTP API design in practice
Last refreshed 2026-09-18.