↑↓ to move ↵ to open Esc to close Browse all tools

Developer guide

localStorage vs Cookies vs IndexedDB: Where Data Goes

A browser offers three places to keep data for a site: cookies, Web Storage with localStorage and sessionStorage, and IndexedDB. They differ in the one way that matters most: cookies travel to the server with requests, the other two never leave the browser unless your code sends them. Here are the size limits, lifetimes and security attributes of each, from the specifications and MDN, and a table to pick the right one.

Three kinds of storage

  • Cookies are small name and value pairs set by a server with a Set-Cookie response header, or by script through document.cookie. The browser stores them and adds them to the Cookie header of later requests to the same site.
  • Web Storage is a key and value store for strings. localStorage is shared by all pages of one origin and survives a browser restart. sessionStorage is partitioned by tab as well as origin, and closing the tab destroys it.
  • IndexedDB is a database in the browser for, in MDN's words, significant amounts of structured data, including files and blobs, with indexes for searching and transactions.

MDN's own advice is that cookies were used for general storage only in the early days of the web, when there was no other option, and that Web Storage and IndexedDB are now recommended for that job.

Side by side

CookieslocalStoragesessionStorageIndexedDB
Typical size4 KB each5 MiB5 MiBShare of disk
Sent to the serverWith every matching requestNoNoNo
LifetimeExpires or Max-Age, else sessionNo expiryUntil the tab closesNo expiry
ScopeDomain and pathOriginOrigin and tabOrigin
Readable by JavaScriptYes, unless HttpOnlyYesYesYes
DataOne stringStringsStringsStructured data, files
APIHeaders, document.cookieSynchronousSynchronousAsynchronous

Sent with every request, or never

This is the real difference. The HTML Standard gives it as one reason Web Storage exists: cookies are transmitted with every request. MDN adds that this can worsen performance, for example on slow mobile connections, especially with many cookies. Take a site with 4,096 bytes of cookies whose pages make 30 requests to its own domain for scripts, images and API calls. Before any header compression, that is 4,096 × 30 = 120 KiB sent upstream for every page view, carrying data the server may never read.

Web Storage and IndexedDB cost nothing on the network. The flip side is that the server cannot see them. If the server needs a value on the first request, before any script has run, such as a session ID or a language choice, it has to be a cookie. The HTTP Header Checker shows the Set-Cookie headers a URL sends, with all their attributes.

Size limits

RFC 6265 asks browsers to support at least 4,096 bytes per cookie, counted over the name, value and attributes together, at least 50 cookies per domain and at least 3,000 cookies in total. These are minimums the RFC asks for, not maximums, but MDN puts the usual per-cookie maximum at 4KB, so design for that.

For Web Storage, MDN gives 5 MiB of localStorage and 5 MiB of sessionStorage per origin, and at most 10 MiB in all browsers. Past that, setItem throws a QuotaExceededError. Keys and values are always strings. Binary data such as an image has to be encoded first, often as Base64, which turns every 3 bytes into 4 characters: a 3 MiB image becomes about 4 MiB of text, most of the limit. The Base64 Encode and Decode tool encodes a file so you can compare the sizes before storing it. IndexedDB stores files and blobs directly and has far larger quotas, which MDN lists by browser. For a 256 GiB disk:

BrowserRuleOn 256 GiB
Chrome, EdgeUp to 60% of total disk153.6 GiB
Firefox, best-effort10% of disk or 10 GiB, whichever is smaller10 GiB
Firefox, persistentUp to 50% of disk, max 8 TiB128 GiB
Safari, macOS 14 and iOS 17Around 60% of disk per origin153.6 GiB

MDN notes that browsers base these quotas on total disk size, not free space, so a site may not actually reach its quota. navigator.storage.estimate() returns an estimate of usage and quota for the current origin.

How long data lasts

A cookie with Expires or Max-Age lives until that time; with both, Max-Age wins. A cookie without either is a session cookie, removed when the session ends, though MDN warns that browsers which restore sessions on restart can keep such cookies indefinitely. localStorage has no expiration time. sessionStorage lasts as long as its tab. In private windows, MDN says localStorage behaves like sessionStorage and everything is deleted when the window closes.

No expiry does not mean forever. By default storage is best-effort: under storage pressure, browsers delete the data of the least recently used origins first, and an evicted origin loses all of its storage at once. Safari goes further when tracking prevention is on: data created by script for a site without user interaction in the last seven days of browser use is deleted, while cookies set by the server are exempt. navigator.storage.persist() asks the browser to keep an origin's data until the user deletes it.

What JavaScript can read

Every script on an origin can read its localStorage, sessionStorage and IndexedDB. That includes third-party scripts the page loads and any script an attacker manages to inject. Cookies are readable through document.cookie too, except one kind: MDN says the HttpOnly attribute forbids JavaScript from accessing the cookie, while the browser still sends it with requests, including those made by fetch(). MDN describes this as a mitigation against cross-site scripting. Whether a login should then live in a token or a server session is a separate decision, covered in JWT vs Session Cookies; if you find a token in storage and want to see what it carries, the JWT Decoder shows its claims and expiry.

Cookie attributes that matter here

  • Secure: sent only over https, except on localhost.
  • HttpOnly: invisible to script, as above.
  • SameSite: Strict sends the cookie only on same-site requests, Lax also on top-level navigations with safe methods such as GET, and None on cross-site requests too, which requires Secure. Some browsers use Lax when the attribute is missing.
  • __Host- prefix: a cookie named like __Host-id must be set with Secure, from an https page, without a Domain, and with Path=/.

How these attributes separate a site's own cookies from third-party ones is explained in First-Party vs Third-Party Cookies.

Which one to use

  • Cookie: the server must see the value on every request, such as a session ID. Make it HttpOnly, Secure and SameSite unless script truly needs it.
  • localStorage: small preferences that only the page uses, such as a theme or a dismissed notice. Never secrets. The HTML Standard sends a storage event to the origin's other open windows when a value changes, which keeps several tabs in sync.
  • sessionStorage: state for one tab, such as a half-filled form or a step in a wizard, that should vanish when the tab closes.
  • IndexedDB: anything large or structured, offline data, files. It is asynchronous, so it does not block the page the way synchronous Web Storage calls do, and MDN notes it is also available in Web Workers, where the HTML Standard does not expose Web Storage.

Frequently asked questions

Is localStorage sent to the server?

No. Web Storage stays in the browser; a page has to read it with JavaScript and send it on purpose. Cookies are different: the browser adds every matching cookie to the Cookie header of each request, which MDN notes can slow pages down when many are set.

How much can I store in localStorage?

MDN gives 5 MiB of local storage and 5 MiB of session storage per origin, and 10 MiB of Web Storage at most. Values are strings, so numbers and objects have to be converted, usually with JSON.stringify. When the limit is reached, setItem throws a QuotaExceededError, so wrap writes in try and catch.

Can JavaScript read HttpOnly cookies?

No. MDN says HttpOnly forbids JavaScript from accessing the cookie, for example through document.cookie. The browser still sends it with requests, including those made by fetch, so the server sees it while an injected script cannot copy it.

Is localStorage safe for login tokens?

It is readable by every script running on the origin, so a single cross-site scripting bug exposes whatever is stored there. An HttpOnly cookie cannot be read by script, which is the reason MDN gives for the attribute. The trade-offs between tokens and server sessions are a separate question, covered in the guide on JWTs and session cookies.

When does the browser delete stored data?

Cookies expire at their Expires or Max-Age time; without either they last for the session. localStorage has no expiry, but under storage pressure browsers evict best-effort data from the least recently used origins, and an evicted origin loses all of its storage at once. Calling navigator.storage.persist() asks for protection from that eviction.

Sources