Browser storage decision flow: localStorage, sessionStorage, IndexedDB, Cookie, Cache API
“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
| Storage | Typical capacity | Lifetime | API | Sent to server | Readable in SSR |
|---|---|---|---|---|---|
| localStorage | 5–10 MB | persistent (until cleared) | sync | no | no |
| sessionStorage | 5–10 MB | until tab closes | sync | no | no |
| IndexedDB | 100s of MB to GB | persistent | async | no | no |
| Cookie | 4 KB / cookie | Expires controlled | sync | always | yes |
| Cache API | 100s of MB to GB | persistent | async | no | no |
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
DateandMapdon’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
onupgradeneededor 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).
HttpOnlyblocks JS access (XSS-resistant).Securerequires HTTPS.SameSitecontrols 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:
Does the server need to see it on every request?
- Yes → Cookie (mind the 4KB cap and CSRF posture).
- No → continue.
Storing HTTP responses verbatim (PWA / offline)?
- Yes → Cache API + Service Worker.
- No → continue.
MB-scale or structured data?
- Yes → IndexedDB (Dexie recommended).
- No → continue.
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.