Stockio

localStorage vs sessionStorage vs IndexedDB Compared

Choose browser storage by data lifetime, size and access pattern. Compare synchronous Web Storage with IndexedDB transactions, migrations, quotas and failure handling.

By Sofia Marchetti

JavaScript Engineer ·

On this page

Browser storage is not a miniature server database with guaranteed permanence. It is a collection of APIs with different lifetimes, execution models and failure modes. Choosing one means deciding how much data loss your interface can tolerate, how often it reads and writes, and whether the browser must search records without loading everything first.

Classify the data before choosing an API

Separate preferences, temporary workflow state and offline records. A theme choice is small and easy to recreate. A multi-step form draft may matter only within one tab. An offline inspection containing hundreds of records needs structured access and a deliberate synchronization strategy. These are different workloads even when every value starts as JSON.

Use the JSON formatter to inspect a representative stored object before defining the persistence layer. Check nesting, duplicated values and approximate payload size. Remove fields that can be derived from other data. Formatting helps inspection, but it does not validate application rules or make untrusted stored data safe.

Define data lifetime explicitly. Should closing a tab discard it? Should signing out remove it? Can the application recover it from the server? Put those answers in the implementation contract. Persistence decisions belong beside your development workflow, rather than being scattered across components as ad hoc storage calls.

All three APIs are scoped by origin in ordinary usage, with additional browser partitioning and embedding policies in some contexts. A different scheme, host or port can mean a different storage area. Never assume a staging site and production site share values, or that an embedded widget receives the same storage context as a top-level page.

Use Web Storage for small synchronous values

localStorage keeps string key-value pairs across ordinary browser sessions until they are removed, cleared or otherwise lost. It suits a small preference or dismissible notice, not a growing activity log. Both reads and writes are synchronous, so repeated serialization and large writes can interrupt main-thread work.

sessionStorage also stores strings synchronously, but its lifetime follows the page session in a particular tab or browsing context. Reloading normally preserves it; closing the tab normally ends that session. A newly opened tab can initially receive a copy from its opener in some circumstances, after which changes are separate. Do not treat it as a secure tab identity mechanism.

Web Storage cannot directly store objects. JSON serialization drops or changes some JavaScript values, and parsing may fail if old or corrupted content is encountered. Version records and validate the fields you actually use. The MDN Web Storage guide explains the execution model and storage-event behaviour.

The storage event notifies other eligible documents when a storage area changes, not the document performing the write. localStorage can support lightweight cross-tab coordination, but that is not a locking protocol. Two tabs performing read-modify-write updates can overwrite each other. Use a transactional design for counters, queues or records whose integrity matters.

Make reads and writes safely optional

A preference should not prevent the page from rendering when storage is blocked. Access, parsing and writes can fail, so isolate them behind a narrow adapter. This example validates a versioned theme preference and treats persistence as an enhancement rather than a startup dependency.

type Theme = "light" | "dark" | "system";
const key = "preferences:theme";

function isTheme(value: unknown): value is Theme {
return value === "light" || value === "dark" || value === "system";
}

export function readTheme(): Theme {
try {
  const raw = window.localStorage.getItem(key);
  if (!raw) return "system";
  const value: unknown = JSON.parse(raw);
  if (typeof value !== "object" || value === null) return "system";
  const record = value as Record<string, unknown>;
  return record.version === 1 && isTheme(record.theme)
    ? record.theme
    : "system";
} catch {
  return "system";
}
}

export function writeTheme(theme: Theme): boolean {
try {
  window.localStorage.setItem(key, JSON.stringify({ version: 1, theme }));
  return true;
} catch {
  return false;
}
}

Call this browser-specific adapter only on the client. In server-rendered applications, decide separately how the first theme is chosen to avoid a flash or markup mismatch. A catch block prevents a crash; it does not solve server rendering or guarantee the preference was saved.

For important drafts, distinguish saved locally, pending save and save failed. Silently returning a fallback is reasonable for a theme but misleading for a user's unsent report. Debounce routine draft writes, cap retained history and provide an export or server sync path when losing the work would be costly.

Choose IndexedDB for records and transactions

IndexedDB is asynchronous and supports structured-clone-compatible values, including many objects and binary data. It organizes records into object stores, with keys and indexes for retrieval. Choose it when you need many independent records, indexed queries, blobs or multiple changes committed together. It is not a general SQL engine, and indexes must match anticipated access patterns.

Design an offline task store around actual queries: tasks by project, tasks awaiting synchronization and a task by identifier. Add only the indexes those queries require. Loading every record and filtering in JavaScript can defeat the reason you chose a structured database. The MDN IndexedDB guide covers the underlying concepts.

A successful individual request does not mean the entire transaction has committed. Report success after transaction completion, and handle aborts and errors. Keep transactions short; awaiting unrelated network work can allow a transaction to become inactive before your next database operation. Fetch remotely first, then perform the necessary local changes together.

Database versions also need migrations. Create or modify stores and indexes during the upgrade transaction. Test an upgrade while an older tab remains open: existing connections can block progress. Handle versionchange by closing stale connections and explain blocked upgrades to users instead of leaving a spinner indefinitely.

Plan for quotas, eviction and sensitive data

There is no single quota number that reliably describes every browser, device and browsing mode. Handle write failures, inspect usage where supported, and treat estimates as planning signals rather than promises. The MDN storage quota reference describes browser differences and eviction considerations.

Durability is conditional even with IndexedDB. Users can clear site data, private browsing has different retention expectations, and storage pressure can cause eviction. A persistence request may reduce some eviction risk when granted, but it is not a backup. Keep irreplaceable records synchronized or exportable and make that limitation visible.

None of these APIs is a safe vault against script running in your origin. An XSS vulnerability can expose readable stored data. Do not store secrets merely because a value is in sessionStorage rather than localStorage. Follow the XSS prevention guidance, minimize sensitive fields and choose authentication mechanisms separately from convenience storage.

On logout, remove account-specific caches and drafts according to product policy. Include schema versions and account boundaries in your keys or records so the next user of a shared device does not inherit another person's workspace. Test denied storage, full storage and stale schemas before calling the persistence layer complete.

Key takeaways

  • Use localStorage for small persistent preferences and sessionStorage for tab-scoped state.
  • Choose IndexedDB for indexed records, larger datasets and atomic changes.
  • Validate stored values and version schemas before trusting old data.
  • Handle failures explicitly when users could lose meaningful work.
  • Treat browser persistence as conditional storage, not a secure backup.
Featured sponsor

Reach designers & developers at work

Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.

Become a sponsor

Keep reading

Related articles

More in JavaScript & Frameworks
JavaScript & Frameworks

10 Best JavaScript Charting Libraries

Compare ten JavaScript chart libraries for dashboards, reports, and interactive data stories. See where each excels, what it costs, and how to choose based on your app and team.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

8 Best React State Management Libraries

Find a React state approach that fits your app, from built-in hooks to dedicated stores, with clear trade-offs around data flow, debugging, async work, and learning effort.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

12 Best and Most Popular JavaScript Libraries

Compare twelve widely used JavaScript libraries for interfaces, data, testing, and everyday development, with clear strengths, trade-offs, and advice for choosing only what your project needs.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

React Server Components Explained for Developers

Learn what React Server Components execute, what crosses the client boundary, and how to structure data access, interactive widgets, and mutations without leaking private data.

Includes: JSON formatterDaniel KimDaniel Kim
JavaScript & Frameworks

useMemo vs useCallback vs React.memo: Fewer Rerenders

Learn when React memoization helps, how prop identity affects rerenders, and why profiling and state placement should come before useMemo and useCallback.

Includes: JSON formatterDaniel KimDaniel Kim
JavaScript & Frameworks

Deep Copy in JavaScript: structuredClone vs Spread

Understand when spread is enough, when structuredClone creates a useful snapshot, and why targeted immutable updates often beat cloning an entire object graph.

Includes: JSON formatterDaniel KimDaniel Kim