StorageGENERALSPEC-EVOLVINGBROWSER-SPECIFIC

Cookies

The only browser store the network sends for you. That single property explains the convenience, the size limits, the scoping attributes and the class of attack built on top of it.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

What makes a cookie different from every other place I could put this, and what does it cost on every request?

The user intent

Someone signs in once and expects to stay signed in — across pages, across tabs, across a browser restart — without the application asking them to prove it again on every click.

The obvious build

A cookie is a small key-value store with an expiry. Set it with document.cookie, read it back later, and treat it as a slightly awkward localStorage.

Why it breaks

It is not a store you read from — it is a store the browser *transmits*. Every request whose URL matches the cookie's Domain and Path carries it, whether that request is your API call, a font, a favicon or an image (Reading a Network Waterfall).

How it breaks in a real browser
  • It is not a store you read from — it is a store the browser *transmits*. Every request whose URL matches the cookie's Domain and Path carries it, whether that request is your API call, a font, a favicon or an image (Reading a Network Waterfall).
  • That transmission is unconditional on the origin of the code making the request, which is the entire mechanism behind cross-site request forgery: the browser attaches the cookie because the URL matched, not because your page asked (Cross-Site Request Forgery).
  • It is small — a few kilobytes per cookie and a bounded number per domain — so the first time someone stores a JSON blob in one they discover a limit that Web Storage does not have.
  • document.cookie is a single string containing every non-HttpOnly cookie for the document, concatenated. Reading one value means parsing all of them, and writing one means a carefully-formatted assignment that looks nothing like a set.
  • A cookie set on the registrable domain rather than the host leaks to every subdomain, including the marketing site and the status page you do not control (The Browser Security Model).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • The server sends Set-Cookie on a response, or script assigns to document.cookie. The browser stores the pair in a cookie jar keyed by domain and path, alongside its attributes.
  • On every subsequent request the browser walks the jar, selects the cookies whose Domain, Path, Secure and SameSite constraints match the request, and serializes them into a single Cookie header. Nothing in your application code is involved.
  • Domain widens scope: omitted, the cookie is sent only to the exact host that set it; set to a parent domain, it is sent to that domain and every subdomain under it. There is no way to narrow it back down for one subdomain.
  • Path narrows scope to URLs under a prefix. It is a matching rule, not a security boundary — same-origin script can read the cookie regardless of which path it is on.
  • Expires gives an absolute date, Max-Age a relative number of seconds. With neither, the cookie is a session cookie and lives until the browser decides the session ended — which, with session restore, can be considerably longer than the user assumes.
  • HttpOnly removes the cookie from document.cookie entirely, so it is present on requests but invisible to script. Secure restricts it to secure transports. SameSite constrains whether it rides along on cross-site requests (Cookies vs Script-Readable Tokens).

What this makes the browser do

And which of it is avoidable.

  • Cookie matching runs per request. For a page pulling a hundred subresources from one origin, that is a hundred jar lookups and a hundred header serializations.
  • The header is sent on the request, which is the direction with the least available bandwidth on most connections, and it is sent before the server can say whether it needed it.
  • Large cookies push request headers past what fits in the first packets, which on a cold connection can add a round trip before the server has the full request (The Critical Rendering Path).
  • Header compression on HTTP/2 and HTTP/3 makes repeated identical cookies much cheaper across requests on a connection, which softens but does not remove the cost (HTTP/1.1 vs HTTP/2 vs HTTP/3 in Networking).
  • Parsing document.cookie in script is a string split over every readable cookie on the document, done from scratch on every access.

The browser sends it, not you

The defining property of a cookie is that it leaves your control the moment it is set. You do not attach it, you do not decide which requests carry it, and you cannot easily tell from the calling code that it is being sent. The browser decides, by matching the request URL against the jar.

This is why a cookie is the right home for session identity — every request to your origin arrives already carrying it, including the very first HTML request, before any of your JavaScript exists (Server-Side Rendering). It is also why the same mechanism is a liability: a request forged by another site, or by injected script on yours, is matched by exactly the same rule.

One cookie, every matching request
stores + attributesmatchesmatchesmatches — and paysmatches unless SameSiteServer: Set-CookieCookie jar (domain + path)GET / (HTML)GET /api/ordersGET /logo.pngRequest from another siteCookie: header attachedServer reads identity
UserLLMAgentToolDataDecisionHumanGuardrail

The attributes, read as scoping rules

Every attribute answers one of two questions: which requests carry this, and how long does it live. Reading them that way makes the behaviour predictable, and makes it obvious why setting a cookie again with different attributes creates a second cookie rather than replacing the first.

The security attributes — HttpOnly, Secure, SameSite — are taught as controls in Security Engineering, where the attack model is the point. Here they matter because they change the exposure profile of this store relative to the other four: a HttpOnly cookie is the only browser storage that a script on your own origin cannot read (Storage Security and Durability).

Two cookies, two very different bargains
1HTTP/1.1 200 OK
2Set-Cookie: sid=9f3a...; Max-Age=1209600; Path=/; Secure; HttpOnly; SameSite=Lax
3Set-Cookie: theme=dark; Max-Age=31536000; Path=/; Secure; SameSite=Lax
4
5# Later, on every matching request — including static assets under /:
6GET /logo.png HTTP/1.1
7Cookie: sid=9f3a...; theme=dark
8
9# Deleting is a set with a past expiry AND identical scope:
10Set-Cookie: theme=; Max-Age=0; Path=/; Secure; SameSite=Lax

sid is HttpOnly, so document.cookie never shows it — that is the point of it. theme is not, because script has to read it to apply the class, which means every script on the origin can read it too. Both are attached to /logo.png, which has no use for either.

What one cookie costs, end to end

It is worth walking the whole path once, because the cost is spread across so many steps that no single one looks expensive. This is the shape of a per-request tax: individually invisible, collectively the reason a page with a fat cookie has slower requests than the same page without one.

The mitigation is not clever engineering — it is scoping. A cookie that only matches the request paths that need it, and only lives as long as it is useful, is cheap. One set at the registrable domain with a long lifetime and a JSON payload is not.

From Set-Cookie to a slower request
  1. 1
    Set

    Server or script writes the cookie into the jar with its attributes

    fails by A missing Secure or a too-wide Domain sets a scope you cannot narrow later without deleting and re-setting

  2. 2
    Match

    For each request, the browser selects cookies by domain, path, transport and SameSite

    fails by Two same-named cookies at different scopes both match, and the server picks one by header order

  3. 3
    Serialize

    Selected cookies are joined into one Cookie request header

    fails by Header grows past what fits early in the connection, adding a round trip before the server has the full request

  4. 4
    Send

    The header travels on the request, in the uplink direction

    fails by On a slow uplink this is the most expensive direction, and it is paid per request rather than once

  5. 5
    Receive

    The server parses the header and looks up the session

    fails by A stale or rotated value produces a 401 that the client experiences as an unexplained sign-out (Session Expiry and the Refresh Race)

  6. 6
    Repeat

    The same match-serialize-send happens for every subresource under the matching path

    fails by Fonts, images and static assets carry identity they have no use for, on every single load

Every step is cheap. The reason cookie weight shows up in a waterfall at all is the last one: it happens on every matching request, forever, and nothing in the calling code makes that visible (Reading the Browser Waterfall in Observability & Performance).

How to build it

Most important first.

  • Put something in a cookie because the server needs it on the request. That is the whole justification, and if it does not apply, one of the other four stores is a better home (Choosing Browser Storage).
  • Keep them small and few. A cookie is a per-request tax; the question is not "does this fit" but "is this worth paying for on every request to this origin for the next year".
  • Scope deliberately. Set the narrowest Domain that works — omit the attribute unless subdomains genuinely need it — and use Path to keep a cookie off request paths that have no use for it.
  • Set an explicit lifetime. Max-Age is easier to reason about than Expires because it does not depend on the client's clock being right (Timezones and Locale Formatting).
  • Let the server set anything that matters, with HttpOnly and Secure, and treat script-set cookies as being for things script owns anyway (What the Frontend Is Responsible For in Auth).
  • Use the async cookie store API where it is available rather than parsing document.cookie, and feature-detect rather than assuming — availability differs by browser.
  • Delete by setting the same name, domain and path with an expiry in the past. A cookie set with different attributes is a *different* cookie, which is why "it will not delete" is usually an attribute mismatch.

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • Cookies have no direct accessibility surface, but they decide what the server renders — a locale or contrast preference stored in a cookie is applied before first paint, where a localStorage value cannot be (Render-Blocking Resources).
  • That makes a cookie the one store capable of preventing a flash of the wrong theme: the server can emit the correct markup rather than script correcting it after paint, which is a real benefit for users sensitive to contrast changes (Contrast, Colour and Motion).
  • Cookie consent interfaces are where this lesson meets accessibility most visibly. A banner that traps focus, is not reachable by keyboard, or renders over content without being announced makes the site unusable before it makes it compliant (Focus Management).
  • Losing a cookie silently signs the user out mid-task. For someone who took several minutes to complete a form with assistive technology, an unexplained redirect to a login page is not a minor inconvenience (Login Redirects and the Open-Redirect Trap).

What can go wrong

Failure modes
  • Two cookies with the same name at different Domain or Path scopes. Both are sent, the server reads whichever it sees first, and the behaviour depends on a header ordering nobody controls.
  • Cookie count or size limits reached: the browser silently drops or evicts cookies for that domain, and the one that disappears is not the one you would have chosen.
  • A session cookie that outlives the session because the browser restored the previous window, leaving a user signed in when they expected not to be (Session Expiry and the Refresh Race).
  • A cookie set by script without Secure on a page that is otherwise secure, which is invisible in development and a downgrade in production.
  • The mitigation failing: SameSite constrains cross-site transmission but does not authenticate anything, so a same-site request forged by injected script on your own origin still carries it (Cross-Site Scripting).
What can arrive out of order
  • A response's Set-Cookie and an in-flight request race: a request that left before the rotation carries the old cookie and may be rejected by a server that has already moved on (Session Expiry and the Refresh Race).
  • Two tabs refreshing a session concurrently can each set a cookie; the last write wins and one tab now holds a token the server no longer recognises (Auth Across Tabs).
Security
  • The browser enforces HttpOnly, Secure, Domain, Path and SameSite absolutely — script cannot read an HttpOnly cookie, and no flag talks the browser out of it.
  • The browser enforces nothing about *meaning*. A cookie value is a string the client can replace; anything the server trusts must be signed, opaque, or looked up server-side (What the Frontend Is Responsible For in Auth).
  • SameSite reduces the set of cross-site requests that carry the cookie, which mitigates classic CSRF; it is a transmission rule and not a replacement for a server-side check (Cross-Site Request Forgery).
  • Any script on the origin reads every non-HttpOnly cookie, exactly as it reads Web Storage. HttpOnly is the only thing on this list that changes that (Storage Security and Durability).
  • A cookie scoped to a parent domain is readable and settable by every subdomain, so a compromised subdomain becomes a cookie-setting position against the main site (The Same-Origin Policy).
Misreads
  • "Cookies are just a small localStorage." The store is incidental; automatic transmission is the point, and it is the reason every other property looks the way it does.
  • "Path protects the cookie from other parts of my site." It scopes which requests carry it. Any script on the origin can read a non-HttpOnly cookie from any path.
  • "SameSite fixed CSRF." It removes most of the classic cross-site vector and does nothing about a forged request originating on your own origin (Cross-Site Request Forgery).
  • "HttpOnly prevents cross-site scripting." It prevents script *reading that cookie*. Injected script can still make requests as the user, and the browser will attach the cookie for it (Cross-Site Scripting).
  • "Setting the cookie again with a new value replaces it." Only if name, domain and path all match. Otherwise you now have two, and the server sees both.
  • "Cookies are going away." Third-party cookie behaviour is changing; first-party cookies remain how the web carries session identity (Storage Security and Durability).

Measuring it, and what changes in the field

How you would see this
  • The Application panel's cookie table shows name, value, domain, path, expiry and every attribute — the fastest way to see that the cookie you thought you deleted is a second one at a different scope (A Mental Model of the Devtools).
  • The Network panel shows the actual Cookie header sent per request, which is the ground truth when a cookie "is not being sent" (Debugging the Network).
  • Request-header size in the waterfall shows the accumulated weight; sort by it once and the surprise is usually a third-party analytics cookie (Third-Party Scripts and the Supply Chain).
  • In the field, track authentication failures and unexpected sign-outs as a class — cookie problems are far more visible in aggregate than in any single session (Real User Monitoring).
Slow device, slow network, large data, old tab
  • On a slow uplink, cookie bytes are expensive in the direction with the least capacity, and they are paid on every request rather than once.
  • On HTTP/1.1 there is no header compression, so a large cookie is re-sent in full on every request; on HTTP/2 and HTTP/3 the repeated value compresses well across a connection.
  • In a cross-site frame, cookie behaviour is currently the most volatile part of the platform: partitioning and third-party restrictions differ by browser and are still changing (Storage Security and Durability).
  • In a long-lived tab, a cookie can be rotated by a background request while your page still holds an older assumption about the session (Auth Across Tabs).
What this costs
  • Automatic transmission is the feature and the liability in one. You get the cookie on every request without writing code, and you get it on requests you did not intend, which is exactly the surface CSRF exploits.
  • HttpOnly removes the cookie from script, which prevents exfiltration by injected code and simultaneously removes your ability to inspect or refresh it in the client. That is a genuine trade, not a free win (Cookies vs Script-Readable Tokens).
  • Narrow Path scoping saves bytes and costs you a matching rule that will eventually surprise someone moving a route.
  • A long Max-Age improves the returning experience and lengthens the window in which a stolen cookie is useful (Session Expiry and the Refresh Race).

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe jar, the matching rules and the attribute semantics are specified and consistent across browsers; a cookie set with the same attributes behaves the same way in Chrome, Firefox and Safari for first-party requests.
  • SPEC-EVOLVINGThird-party cookie behaviour, partitioning and the maximum accepted lifetime for script-set cookies are actively changing and differ today: Safari caps script-set cookie lifetime aggressively while Chrome and Firefox apply different rules again, so verify against current browser documentation rather than trusting a lesson.
  • BROWSER-SPECIFICPer-cookie size and per-domain count limits are implementation policy rather than specification minimums, and browsers differ in whether they drop the oldest cookie, the least recently used one, or reject the write.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Software Design — a value that travels implicitly on every call is an ambient parameter, with the same testability and reasoning costs it has in any other codebase.