HTTP Cookies
The original client-side storage — small, automatically sent with every request, and still how a stateless protocol remembers you.
Overview
HTTP CookiesSmall (≤4KB) key-value pairs automatically sent with every HTTP request to the domain that set them.Learn more are small key-value pairs that the browser automatically attaches to every HTTP request sent to the domain that set them. Cookies predate localStorage by roughly two decades, and they remain the only client-side storage mechanism a server can both set and read directly through plain HTTP headers — no JavaScript required on either side.
Why It Exists
HTTP itself is stateless — by design, a server has no memory of any previous request from the same browser. Cookies exist specifically to bridge that gap: they're how a server recognizes 'this is the same browser that logged in five minutes ago' without inventing some other out-of-band mechanism. Every other client-storage technology that came after — localStorage, IndexedDB — exists on the client only; cookies are the one storage mechanism built into the request/response cycle itself.
Real World Example
After a successful login, a server responds with a `Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax` header. From that point on, every request the browser makes to that same domain automatically includes `Cookie: session_id=abc123` — the client does nothing extra, and the server looks up the session on every request without needing any client-side code to remember and resend a token.
Example Data
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax; Max-Age=3600
| Attribute | Value | Effect |
|---|---|---|
| Name/Value | session_id=abc123 | The actual data being stored |
| HttpOnly | (flag) | JavaScript cannot read this cookie at all |
| Secure | (flag) | Only sent over HTTPS, never plain HTTP |
| SameSite | Lax | Not sent on most cross-site requests |
| Max-Age | 3600 | Expires automatically after 1 hour |
Cookie Attributes, One at a Time
HttpOnly
Blocks client-side JavaScript from reading the cookie's value entirely — `document.cookie` simply won't show it. This is the single most effective defense against session-token theft via XSS: even if an attacker injects a script into your page, that script has no way to read an HttpOnly cookie and exfiltrate it.
Set-Cookie: session_id=abc123; HttpOnly
Secure
Tells the browser to only ever send this cookie over HTTPS, never plain HTTP. Without it, a cookie set on a secure page could still leak in cleartext if any part of the site is ever loaded over an unencrypted connection.
Set-Cookie: session_id=abc123; Secure
SameSite
Controls whether the cookie is sent along with cross-site requests — the main defense against CSRF. `Strict` never sends it cross-site (safest, but breaks some legitimate flows like clicking a link from an email). `Lax` sends it on top-level navigation but not on cross-site subrequests (the modern default, and the right choice for most cookies). `None` sends it everywhere, but requires `Secure` and is meant specifically for legitimate cross-site use cases like embedded widgets.
Set-Cookie: session_id=abc123; SameSite=Lax
Domain & Path
Domain controls which hosts the cookie is sent to (omitting it scopes the cookie to the exact host that set it; setting it explicitly, e.g. `.example.com`, shares it across subdomains). Path further scopes it to a specific URL prefix, so a cookie set with `Path=/admin` is never sent on requests to `/checkout`.
Set-Cookie: session_id=abc123; Domain=.example.com; Path=/
Expires & Max-Age
Without either, a cookie is a 'session cookie' — deleted when the browser closes. `Max-Age` (in seconds) or `Expires` (an absolute date) makes it persistent, surviving browser restarts until that time is reached or it's explicitly cleared.
Set-Cookie: session_id=abc123; Max-Age=3600
Diagram
A cookie set once, sent automatically on every request after that
Server responds
Set-Cookie: session_id=abc123
Browser stores it
scoped by domain, path, and attributes
Every future request
Cookie: session_id=abc123 — sent automatically
Server reads it
no client-side code required
Syntax
// Setting a cookie via an HTTP response header (server-side) Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax; Max-Age=3600 // Reading non-HttpOnly cookies client-side document.cookie; // "theme=dark; consent=true" — a raw, semicolon-separated string // Setting a non-HttpOnly cookie client-side document.cookie = "theme=dark; Max-Age=31536000; Path=/";
Common Mistakes
Manually parsing the raw document.cookie string with a naive split(';')
Why: The cookie string format has quoting and encoding edge cases a naive split gets wrong — values containing certain characters can silently corrupt the parse.
Fix: Use a well-tested cookie-parsing library, or your framework's built-in cookie utilities, instead of hand-rolled string splitting.
Leaving a session or auth cookie without the HttpOnly flag
Why: Without it, any XSS vulnerability on the page can read the cookie directly via JavaScript and exfiltrate it — the exact same theft vector that makes localStorage unsafe for tokens.
Fix: Mark every authentication-related cookie HttpOnly so it's simply invisible to JavaScript, XSS included.
Not setting SameSite explicitly and assuming the browser default is correct
Why: Default behavior has changed across browser versions, and a cookie genuinely needed for a cross-site flow (an embedded widget, a third-party integration) can silently stop working if the default doesn't match what the use case actually needs.
Fix: Set SameSite deliberately on every cookie — Lax for most site cookies, None + Secure only for cookies with a real cross-site requirement.
Interview Questions
What's the core difference between a cookie and localStorage?
A cookie is automatically sent with every matching HTTP request and can be set directly by the server via a response header. localStorage is purely client-side — nothing in it is ever sent to the server automatically, and only JavaScript can read or write it.
What does the HttpOnly flag protect against, and what doesn't it protect against?
HttpOnly stops client-side JavaScript from reading the cookie, which neutralizes token theft via XSS. It does nothing against CSRF — a forged cross-site request still automatically carries the cookie, since the browser attaches it regardless of which page triggered the request. SameSite is the mitigation for that, not HttpOnly.
A cookie needs to be shared with a third-party iframe embedded on other sites, but a security review flags it. What's the trade-off?
Sharing a cookie with a cross-site embed requires SameSite=None, which also mandates Secure. But setting SameSite=None deliberately gives up the CSRF protection that Lax or Strict would have provided, so that cookie now needs its own explicit CSRF defense — typically a separate CSRF token validated server-side — rather than relying on SameSite to do that job.
Production Best Practices
Do
✓Mark every authentication or session cookie HttpOnly and Secure.
✓Set SameSite explicitly on every cookie instead of relying on the browser's default.
✓Scope cookies as narrowly as possible with Domain and Path.
Don't
✗Don't parse document.cookie by hand — use a real cookie-parsing utility.
✗Don't set SameSite=None unless there's a genuine cross-site requirement, and always pair it with Secure.
✗Don't store anything larger than a small token in a cookie — every cookie is resent on every matching request, so size is a real bandwidth cost.
Comparison
| Blocks JS Access | Requires HTTPS | Sent Cross-Site | |
|---|---|---|---|
| HttpOnly | Yes | No | Depends on SameSite |
| Secure | No | Yes | Depends on SameSite |
| SameSite=Strict | No | No | Never |
| SameSite=Lax | No | No | Top-level navigation only |
| SameSite=None | No | Yes (required) | Always |