Skip to main content

Browser storage — what changes depending on where you put it

There is more than one place to put a value in the browser. Which one you pick changes whether it reaches the server, whether another tab can see it, and when it disappears.

When you pick the wrong one, the symptom is hard to read. Login state does not carry to another tab; things work over http but the value vanishes over https; you get told you are out of space. None of these are about whether the write succeeded — they are about what unit the storage is divided by.

What you'll learn in this chapter

  • Comparing three mechanisms across "does it reach the server", "what separates it", "how long it lasts", "how the size limit applies", and "do tabs share it"
  • That cookies and Web Storage divide along opposite lines
  • That the capacity numbers run in opposite directions as floors and ceilings
  • Where to put authentication tokens
Prerequisites

Builds on the three components of an origin from Origins and CORS and the attributes from Cookies. This chapter adds a third boundary — storage — alongside those two.

What this chapter does not cover

TopicWhere it lives
Data that does not fit in 5 MiB, and working offlineIndexedDB and the Cache API
The conditions under which stored data disappears (eviction)Same as above
How storage divides when embedded in another siteSame as above. Implementations do not key on origin alone
Token storage and refresh flows in an SPAReact Guide — SPA authentication
What cookie attributes themselves meanCookies

Comparing three mechanisms across five axes

The three under comparison are localStorage, sessionStorage, and cookies. Each axis gets its own small table. Collapsing them into one large table would put values that mean different things into the same column and invite misreading.

1. How it reaches the server

MechanismHow it reaches the server
localStorageIt does not. Only when your code explicitly puts it in a body or header
sessionStorageSame as above
CookiesThe browser attaches them automatically when the conditions match

The difference shows up most clearly in the first HTML of a server-rendered page. At the moment the server assembles that HTML, cookies are the only thing it can read — Web Storage lives inside the browser only. If you want the first paint to differ by login state, the deciding value has to be in a cookie.

2. What separates them

MechanismKeyschemeport
localStorage / sessionStorageOriginIncludedIncluded
Cookieshost + pathIgnored (Secure narrows it)Ignored

Chapter 26 said "a cookie's scope is not the origin". Web Storage is the reverse — it is the origin. Same host, different scheme, and you get a different area.

A value written to localStorage on https://example.com
cannot be read from http://example.com (a different origin)

MDN states that "for a site loaded over HTTP … localStorage returns a different object than … over HTTPS". As for port, the spec defines the storage key as a tuple of an origin, so it is included, but the behavior MDN spells out is scheme only.

This asymmetry is the theme of this part. Both are "storing in the browser", yet cookies ignore scheme and port and reach widely, while Web Storage looks at both and divides narrowly. Carrying the instincts from one over to the other causes incidents.

3. How long it lasts

MechanismWhen it goes away
localStorageIt has no expiration
sessionStorageWhen the tab closes
CookiesAt Expires / Max-Age. With neither, when the browser closes

MDN says of localStorage that "the stored data is saved across browser sessions" and "localStorage data has no expiration time".

But having no expiration is not the same as never disappearing. Browsers discard the oldest data when storage runs tight, and some implementations delete data from origins that have gone unused for a while as part of tracking prevention. IndexedDB and the Cache API covers that machinery.

4. How the size limit applies

Listing the numbers alone invites misreading. Each row states what the value applies to and whether it is a floor or a ceiling.

MechanismValueWhat it applies toDirection
localStorage5 MiBThe whole area per originCeiling
sessionStorage5 MiBSame as aboveCeiling
Cookies4096 bytesPer cookie (name + value + attributes combined)Floor
Cookies50 cookiesPer domainFloor

The cookie numbers are the "minimum capabilities" RFC 6265 asks implementations to provide — not ceilings. An implementation is free to allow more. The 5 MiB for Web Storage, by contrast, is a ceiling the Storage Standard assigns per storage endpoint.

What the limit applies to differs — "one cookie" versus "the whole area" — and the directions are opposite. Read them as the same kind of limit and you end up thinking it is safe to pack just under 4 KB into a cookie, or that Web Storage's 5 MiB is per key.

You also cannot add localStorage and sessionStorage up to "10 MiB". sessionStorage divides per tab, so summing it per origin has no meaning.

5. Whether tabs share it

MechanismVisible from another tab?
localStorageYes. Changes from other tabs arrive via the storage event
sessionStorageNo. Independent per tab
CookiesYes

The unit that divides sessionStorage is not "a tab" in the spec but a traversable navigable. It nearly matches the tab as a piece of browser UI, but what the spec regulates is the unit whose history you can traverse. The Storage Standard states that "Session storage buckets must be cleared as traversable navigables are closed".

MDN notes that "Opening a page in a new tab or window creates a new session … which differs from how session cookies work". A window opened with window.open receives a copy of the values as of that moment, but the two move independently afterward.

HttpOnly is not a storage location

You sometimes see HttpOnly lined up as a "fourth place to store things". It is a cookie attribute, and it belongs to the cookie row of the tables above.

And what it blocks is reading, nothing else. Cookies attach to requests automatically, so a script running through an XSS hole can send a request carrying that cookie even without being able to read its value. Do not let a comparison table of storage locations revive the misreading that chapter 26's "common misconceptions" knocked down.

Choosing not to store

You can hold a value in a JavaScript variable and store it nowhere. It is not in the five-axis tables above: on axis 1 it behaves like Web Storage, axis 3 has no applicable value, and axes 4 and 5 come out blank.

It does become a point of comparison when deciding where to put authentication tokens.

PropertyIn-memory
On page reloadGone
Left on disk?No
Readable via XSS?Yes (it is in the same execution context)

"Not storing" is not a defense against XSS. What it helps with is only the path where a value left on the device gets picked up later.

Where to put authentication tokens

The OWASP Session Management Cheat Sheet names the prohibition explicitly.

Do not store authentication tokens, session IDs, JWTs, refresh tokens, or any credential in localStorage or sessionStorage.

The reason connects back to axis 5 and to HttpOnly. Web Storage is readable from JavaScript, so the moment XSS lands, the value walks out. With an HttpOnly cookie the value cannot be read. The damage is smaller by exactly that much.

The procedure for deciding is as follows.

Holding a short-lived access token in memory and putting the long-lived refresh token in an HttpOnly cookie is what this procedure arrives at. React Guide — SPA authentication covers the implementation.

When 5 MiB is not enough

None of the three mechanisms suits large data, and the reason differs for each.

  • Web Storage has a 5 MiB ceiling on the whole area, and its API is synchronous. Reading or writing a large value stalls the main thread for the duration
  • Cookie limits are far smaller, and cookies attach to requests automatically, so putting a large value there makes the traffic itself heavy

"Things that do not fit in 5 MiB" and "things you want to work offline" are the entry points to the next chapter.

Common misconceptions

"localStorage shares values between https and http" — It does not. The key for Web Storage is the origin, which includes the scheme. Reasoning from cookie instincts gets this backwards.

"Cookies are capped at 4096 bytes per domain" — 4096 bytes is per cookie, and it is a floor implementations should provide. The floor for count is 50 per domain, so multiplying them out lands near 200 KB.

"sessionStorage lasts until the browser closes" — Until the tab closes. Opening the same origin in another tab does not share the values.

"localStorage has no expiration, so it never disappears" — It does. It only lacks an expiry setting; it is still subject to storage pressure and to tracking-prevention deletion.

"Putting a token in sessionStorage is safer than localStorage" — Against XSS they are the same. Both are readable from JavaScript. What differs is how long it persists, not the defense at the moment the attack lands.

Exercises

Q1. You ran the following on https://example.com. What does localStorage.getItem('theme') return when you then open http://example.com?

localStorage.setItem('theme', 'dark');
Answer and explanation

Answer: It returns null.

The key that divides Web Storage is the origin, and the origin includes the scheme. https://example.com and http://example.com are different origins, so they hold separate areas.

The host is the same, so a cookie would be sent to both unless Secure were set. The same "store it in the browser" produces opposite results for cookies and Web Storage — that is the theme of this chapter.

If you develop against http://localhost and run production over https, this difference surfaces as "settings do not carry over, but only in production".

Q2. You are building a draft-saving feature. Each draft is around 200 KB and there are at most 30 of them. Can you choose localStorage?

200 KB × 30 drafts = about 6 MB
Answer and explanation

Answer: No. It exceeds the 5 MiB ceiling per origin.

The 5 MiB for localStorage is a ceiling on the whole area. It is not per key, so you judge against the total across all 30. About 6 MB is over the ceiling, and the write throws partway through.

On top of that, the localStorage API is synchronous, so reading or writing a 200 KB string stalls the main thread. Even if the count fit under the ceiling, that alone is reason to avoid it.

Data at this scale belongs in IndexedDB. It is asynchronous, and its capacity limits follow a different system. The next chapter covers it.

Q3. You are reviewing an implementation that saves the access token to localStorage after login. The author explains, "we have CSP in place as an XSS defense, so it is fine." How do you respond?

Answer and explanation

Answer: CSP reduces XSS but does not eliminate it. What changes with the storage location is how much damage lands when it does happen.

OWASP states: "Do not store authentication tokens, session IDs, JWTs, refresh tokens, or any credential in localStorage or sessionStorage." That guidance does not assume anything about CSP.

What you compare is not "the probability that XSS occurs" but "what walks out when it does".

StorageWhen XSS lands
localStorageThe value itself can be read out and sent elsewhere
HttpOnly cookieThe value cannot be read. Requests carrying that cookie can still be sent

Both cause damage, but once the value is out, the attacker can replay the token on their own machine. It works after the browser closes and from a different device. With HttpOnly, the attack stays confined to that page session.

Holding a short-lived access token in memory and putting the refresh token in an HttpOnly cookie is the design that accounts for this difference.

Summary

  • Choose among the three mechanisms by "does it reach the server", "what separates it", "how long it lasts", "how the size limit applies", and "do tabs share it"
  • Cookies and Web Storage divide along opposite lines. Cookies ignore scheme and port; Web Storage is the origin, so it looks at both
  • The capacity numbers run in opposite directions. The 4096 bytes for cookies is a floor per cookie; the 5 MiB for Web Storage is a ceiling on the whole area
  • sessionStorage is independent per tab. It does not carry to another tab
  • Having no expiration is not the same as never disappearing
  • HttpOnly is not a storage location but a cookie attribute, and what it blocks is reading
  • Keep credentials out of Web Storage. Short-lived tokens go in memory, long-lived ones in an HttpOnly cookie
Related references