Browser storage decision flow: localStorage, sessionStorage, IndexedDB, Cookie, Cache API

5 min read

“Persist user actions on the device.” “Keep them logged in.” “Make images viewable offline.” All of these end up at browser storage — but five distinct APIs are available and they behave very differently. This article compares them on the axes that actually matter and gives a decision flow for picking one.

The five at a glance

StorageTypical capacityLifetimeAPISent to serverReadable in SSR
localStorage5–10 MBpersistent (until cleared)syncnono
sessionStorage5–10 MBuntil tab closessyncnono
IndexedDB100s of MB to GBpersistentasyncnono
Cookie4 KB / cookieExpires controlledsyncalwaysyes
Cache API100s of MB to GBpersistentasyncnono

The decision turns on: does the server need to see it, how big is the data, how long should it live, and does SSR need it?

localStorage: lightweight persistent data

The most-used. Right for theme preference, last-opened tab, simple UI state.

  • 5–10 MB depending on the browser (Chrome ≈ 10 MB).
  • Sync API — heavy writes block the main thread.
  • Strings only; objects must be serialized via JSON.stringify.
  • Per-origin scope.

localStorage gotchas

  • JSON serialization means Date and Map don’t round-trip unless you write your own revival logic.
  • Sync writes near the quota cap freeze the UI on mobile.
  • Safari Private Browsing throws on writes (QuotaExceededError). Wrap writes in try-catch.

sessionStorage: tab-scoped scratchpad

Same API as localStorage, but cleared when the tab closes.

  • Save in-progress form input so it survives a refresh.
  • Step state for a multi-page wizard.
  • “Back to previous search” continuity.

It’s the right place for state you genuinely don’t want persisted long-term.

IndexedDB: large structured data

Hundreds of MB to GB, intended for offline document caches, image stores, large JSON tables.

  • Async API (callback / Promise).
  • Indexes, transactions, cursors — a real KV store.
  • The native API is awkward; in practice everyone uses Dexie or idb.
  • Pairs naturally with Service Worker via Workbox.

Where IndexedDB fits

  • Offline-readable documents, mail, messages.
  • Locally-stored user uploads.
  • Search indexes for offline-first apps.

IndexedDB gotchas

  • Low-level API is complex. Forgetting onupgradeneeded or mishandling transactions silently drops data.
  • Some browsers evict it without explicit user permission (Safari’s eviction policies).
  • DevTools “Storage” tab clears it for testing — convenient.

Cookie: state shared with the server

The only storage that rides along on every HTTP request.

  • Session IDs, CSRF tokens, auth flags.
  • 4 KB per cookie, 50–180 total (browser-dependent).
  • HttpOnly blocks JS access (XSS-resistant).
  • Secure requires HTTPS.
  • SameSite controls CSRF behavior.

When you actually need cookies

  • Session authentication (server identifies the session).
  • CSRF tokens.
  • A/B-test bucket assignments that the server also reads.

When cookies are wrong

  • Large data (4 KB cap): use localStorage or IndexedDB.
  • JS-only data: cookies add overhead to every request — wasteful.
  • Cross-origin sharing: 3rd-party cookie support is shrinking (Chrome paused but didn’t reverse the deprecation; Safari and Firefox have it off by default).

Cache API: HTTP responses as cache entries

Combined with a Service Worker, stores network responses verbatim.

  • cache.put(request, response) keeps a request/response pair.
  • Ideal for offline PWAs (HTML/CSS/JS, image caches).
  • Operates on Request / Response objects directly — no JSON conversion needed.
  • Workbox abstracts the patterns.

Where Cache API fits

  • App shell (HTML/CSS/JS) for offline-first.
  • Thumbnail caches.
  • API responses with stale-while-revalidate semantics.

For storing JSON data on its own, IndexedDB is a better fit (indexable, mutable). Cache API thinks in HTTP units.

Decision flow

Run through these in order:

  1. Does the server need to see it on every request?

    • Yes → Cookie (mind the 4KB cap and CSRF posture).
    • No → continue.
  2. Storing HTTP responses verbatim (PWA / offline)?

    • Yes → Cache API + Service Worker.
    • No → continue.
  3. MB-scale or structured data?

    • Yes → IndexedDB (Dexie recommended).
    • No → continue.
  4. Should it survive tab close?

    • Yes → localStorage.
    • No → sessionStorage.

Use-case cheat-sheet

  • Theme (dark/light) → localStorage.
  • In-progress form draft → sessionStorage.
  • Offline-readable docs → IndexedDB (+ Service Worker).
  • PWA app shell → Cache API (+ Service Worker).
  • Login session → Cookie (HttpOnly + Secure + SameSite).
  • CSRF token → Cookie + meta-tag combination.
  • Search history (small) → localStorage; (large) → IndexedDB.
  • Offline image viewer → IndexedDB (binaries) or Cache API (HTTP units).

Capacity and lifecycle hazards

Browsers may evict storage without warning:

  • Safari: localStorage / IndexedDB / Cache API can be cleared after 7 days of no visits, per the tracking prevention rules.
  • Chrome / Firefox: LRU eviction under disk pressure.
  • To opt out, request explicit persistence with navigator.storage.persist().

“localStorage means persistent” is too optimistic. Assume it can disappear and design a server-side recovery path.

Summary

The five storage APIs are different trade-offs of capacity × lifetime × access pattern. Cookies are special because they ride HTTP requests; IndexedDB and Cache API are async and capacious; localStorage and sessionStorage are easy but capped. Decide first whether the server needs the data, and the rest of the choice falls out quickly.

To check actual byte sizes when planning a quota, the byte converter tool makes the KB/MB conversions concrete.