Skip to content

Storage options compared

Cookies were introduced in 1994 and remain the only client-side store that participates in the HTTP request cycle. The browser attaches matching cookies to every request automatically — making them the standard mechanism for session tokens and server-side authentication.

  • Capacity: 4 KB per cookie; browsers limit total cookies per domain to roughly 50 (varies by browser).
  • API style: Synchronous. Read via document.cookie (returns a semicolon-separated string of all cookies); write by assigning a Set-Cookie-formatted string to document.cookie.
  • Persistence: Configurable. Session cookies expire when the browser is closed. Persistent cookies carry an Expires or Max-Age attribute.
  • Primary use case: Session tokens, user identity, server-side feature flags, and any value the server must see on every request.

localStorage is the simplest persistent client-side store. Data survives browser restarts and is shared across every tab and window on the same origin.

  • Capacity: 5–10 MB per origin (5 MB is the practical safe limit; exact quota varies by browser).
  • API style: Synchronous. localStorage.setItem(key, value), localStorage.getItem(key), localStorage.removeItem(key), localStorage.clear().
  • Persistence: Indefinite — data remains until the user clears site data or code calls clear().
  • Primary use case: User preferences, theme settings, cached UI state, and small persisted values that do not need to be sent to a server.

sessionStorage shares the same synchronous API as localStorage but is scoped to a single browser tab.

  • Capacity: 5–10 MB per origin (same limits as localStorage).
  • API style: Synchronous — identical interface to localStorage.
  • Persistence: Tab-lifetime only. Closing the tab or navigating away from the origin clears the data. Other tabs cannot read it even if they share the same origin.
  • Primary use case: Wizard or multi-step form state, per-tab navigation history, temporary data that must not bleed across tabs.

IndexedDB is a full transactional database inside the browser. It stores structured JavaScript objects (not just strings), supports secondary indexes, and exposes a Promise-friendly async API (via the native event model or libraries like idb).

  • Capacity: Browsers grant quota dynamically. In practice, origins can store hundreds of megabytes to several gigabytes depending on available disk and browser policy.
  • API style: Asynchronous. All operations are non-blocking: IDBObjectStore.put(), IDBObjectStore.get(), cursor iteration, and so on — all return IDBRequest objects (wrappable in Promises).
  • Persistence: Persistent by default (best-effort). Can be upgraded to durable storage via navigator.storage.persist().
  • Primary use case: Offline apps, large datasets, structured records that need querying, draft storage, local-first applications.

The Cache API stores Request/Response pairs. It was designed for service workers so that network responses can be intercepted, cached, and served while the device is offline.

  • Capacity: Shares the same quota pool as IndexedDB and OPFS (the “bucket” storage). Typically hundreds of megabytes available.
  • API style: Asynchronous. caches.open(name) returns a Promise<Cache>. Then cache.put(request, response), cache.match(request), cache.delete(request).
  • Persistence: Best-effort (can be evicted under storage pressure) unless the origin has been granted persistent storage.
  • Primary use case: Precaching app shells for service workers, runtime caching of API responses, offline-first PWAs.

OPFS gives every origin a sandboxed, private area of the device file system — no permission prompt required. Unlike the public File System Access API, OPFS files are never visible in the user’s file picker.

  • Capacity: Shares the same quota pool as IndexedDB and Cache API.
  • API style: Mostly asynchronous (FileSystemDirectoryHandle, FileSystemFileHandle), with a high-performance synchronous interface (createSyncAccessHandle()) available exclusively inside Web Workers.
  • Persistence: Best-effort by default; can be made persistent.
  • Primary use case: SQLite-in-WASM, large binary assets, media files, any use case that benefits from file-level random access or a synchronous I/O path in a Worker.
CookieslocalStoragesessionStorageIndexedDBCache APIOPFS
Typical capacity4 KB5–10 MB5–10 MBHundreds of MBHundreds of MBHundreds of MB
API styleSyncSyncSyncAsyncAsyncAsync (+ sync in Worker)
Sent with HTTP?YesNoNoNoNoNo
Persists across tabsYesYesNoYesYesYes
Persists across restartsConfigurableYesNoYesYesYes
Can be evicted?No (expires)NoNoYes (best-effort)Yes (best-effort)Yes (best-effort)
Primary use caseAuth / serverUI statePer-tab stateStructured app dataNetwork cachingFile / WASM I/O
OptionBenefitCost
CookiesSent with every HTTP request automatically, good for server-side authOnly ~4 KB, and adds payload to every request even when unneeded
localStorage / sessionStorageSimple API, usable immediately with no setupSynchronous, blocks the main thread, and capacity is limited to roughly 5–10 MB
IndexedDBHandles large amounts of data, queryable, async so it doesn’t block the threadMore complex API — you manage transactions and schema versioning yourself
Cache API / OPFSGreat for offline assets and large binary filesNeeds a service worker or Worker context to get the full benefit
  • Using cookies for general UI state — cookies are sent with every HTTP request, needlessly bloating requests for data that has nothing to do with the server. Use localStorage instead.
  • Using localStorage for a large dataset that needs querying — localStorage only stores strings, has no indexes, and is synchronous, so it doesn’t scale. Move to IndexedDB from the start.
  • Forgetting that every best-effort store can be evicted — assuming IndexedDB or the Cache API persists like localStorage, when by default they are best-effort and the browser can delete them when disk space runs low.
Which storage type is automatically included in HTTP requests sent to the matching domain?
A user closes a browser tab. Which stores still hold their data?
Which two storage APIs share the same synchronous read/write interface?
An app needs to store 200 MB of structured records that must survive browser restarts. Which API is the best fit?