IndexedDB vs localStorage: Which Browser Storage to Use

Pick localStorage for a few small settings: it stores strings only, about 5 MiB per origin, and every call blocks the page until it finishes. Pick IndexedDB for real data: it is asynchronous, transactional, stores objects, dates and files, and its quota is a share of the disk. The demo below writes one note to both.

The short version
QuestionlocalStorageIndexedDB
What can it hold?Strings onlyAnything the structured clone algorithm copies, including Blob and File
SizeUp to 5 MiB per originA share of free disk, decided per browser
Blocks the page?Yes, every call is synchronousNo, results arrive by events or promises
Transactions and indexesNoYes
Usable in a Web WorkerNo, it is a property of windowYes
Best forTheme, last tab, a draft stringOffline data, caches, files, anything you query

Updated

Both APIs keep data in the visitor's browser after the tab closes, and both are scoped to the page's origin. The difference is weight. localStorage is a tiny synchronous string map you can learn in a minute; IndexedDB is a small database with versions, object stores and transactions. This page shows the real limits from the documentation and lets you watch what each one returns.

Save one note to both stores

This demo runs only in your browser. It writes under the key we-demo-note and a database called we-demo-db, and the Clear button removes both.

Note to save: Call the printer on Friday

Nothing read yet. Save a note, then press Read back.

Storage limits by browser

Figures as documented on MDN's storage quotas page. IndexedDB, the Cache API and the origin private file system share the same quota.

Quota per origin
BrowserlocalStorageIndexedDB (best-effort)IndexedDB (persistent)
Chrome, Edge and other Chromium5 MiBUp to 60% of total diskUp to 60% of total disk
Firefox5 MiBSmaller of 10% of disk or 10 GiB per site groupUp to 50% of disk, capped at 8 TiB
Safari browser app5 MiBAbout 60% of total diskAbout 60% of total disk
Apps that embed web content (WebKit)5 MiBAbout 15% of total diskAbout 15% of total disk

The same save in both APIs

localStorage: stringify, guard, parse

The try...catch handles a full store or blocked storage. The date has to be rebuilt by hand after parsing.

function saveNote(text) {
  const note = { text, savedAt: new Date().toISOString() };
  try {
    localStorage.setItem("note", JSON.stringify(note));
    return true;
  } catch (err) {
    console.warn("Could not save:", err.name); // QuotaExceededError or SecurityError
    return false;
  }
}

function loadNote() {
  const raw = localStorage.getItem("note"); // a string or null
  if (!raw) return null;
  const note = JSON.parse(raw);
  return { ...note, savedAt: new Date(note.savedAt) };
}

IndexedDB: open, upgrade, write in a transaction

onupgradeneeded runs the first time and whenever the version number goes up; that is the only place to create object stores.

function openDb() {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open("notes-db", 1);
    req.onupgradeneeded = () => {
      req.result.createObjectStore("notes", { keyPath: "id" });
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

async function saveNote(text) {
  const db = await openDb();
  return new Promise((resolve, reject) => {
    const tx = db.transaction("notes", "readwrite");
    tx.objectStore("notes").put({ id: "latest", text, savedAt: new Date() });
    tx.oncomplete = () => resolve(true);
    tx.onerror = () => reject(tx.error);
  });
}

Ask how much room you have, and ask to keep it

estimate() returns approximate byte counts. persist() resolves to true if the browser granted persistent storage.

const { usage, quota } = await navigator.storage.estimate();
console.log(`Using ${(usage / 1e6).toFixed(1)} MB of ${(quota / 1e9).toFixed(1)} GB`);

if (navigator.storage.persist) {
  const kept = await navigator.storage.persist();
  console.log(kept ? "Data will not be evicted automatically" : "Best-effort storage");
}

Why synchronous matters

When code calls localStorage.getItem, the browser stops running other JavaScript until the value comes back from disk. For a theme name nobody notices. For a few megabytes of JSON parsed on every page load, it shows up as a pause before the page responds to taps.

IndexedDB hands back a request object straight away and fires success or error later. That shape is more code to write, which is why most projects use a promise wrapper, but it never holds the page hostage.

Strings vs structured clone

localStorage turns everything into text. Store the number 3 and you read back the string "3"; store an object without JSON.stringify and you read back "[object Object]".

IndexedDB copies values with the structured clone algorithm, so a Date stays a Date, a Map stays a Map, and a Blob or File can be saved as it is. That is what makes it suitable for offline images, recorded audio or drafts with attachments.

A simple rule for choosing

If the data fits in one short line and you read it once at start-up, localStorage is fine. If you would want to search it, count it, page through it, store binary files in it or update it from a service worker, use IndexedDB.

Server data is a third option that often beats both: if the person expects to see the same thing on another device, it belongs in your database, with the browser store only as a cache.

How it works

  1. Type a note and press Save to both. The page writes it to localStorage as JSON text and to an IndexedDB object store as a real object.
  2. Press Read back. The result panel shows the type each API returned: a string from localStorage, an object with a working Date from IndexedDB.
  3. Press Check quota to ask navigator.storage.estimate() how much this origin uses and may use. The numbers are estimates.
  4. Press Clear demo data to remove the demo key and delete the demo database. Nothing leaves your device.

Tips

Frequently asked questions

How much can localStorage hold?

MDN documents a limit of 5 MiB of local storage and 5 MiB of session storage per origin, 10 MiB in total. Because values are UTF-16 strings, the number of characters you fit can be lower than you expect. Going over the limit throws a QuotaExceededError.

How much can IndexedDB hold?

It depends on the browser and the disk. Chromium browsers allow an origin up to 60% of total disk size. Firefox allows the smaller of 10% of the disk or 10 GiB per site group in best-effort mode. Safari browser apps allow about 60% of the disk. Use navigator.storage.estimate() to read the real figure.

Is IndexedDB faster than localStorage?

For a single tiny value, localStorage is usually quicker to write in code and fast enough to run. The problem is that it is synchronous, so large reads and writes freeze the main thread. IndexedDB does the work off the main flow and tells you when it finishes, which keeps scrolling and typing smooth.

Can the browser delete my IndexedDB data?

Yes. Storage is best-effort by default, so the browser may evict an origin's data when the disk is under pressure. Asking navigator.storage.persist() switches to persistent mode, where only the person can clear it. Firefox shows a permission prompt; Chrome and Safari decide from how the person uses the site.

Should I use localStorage for a login token?

No. Any script on the page, including an injected one, can read localStorage and IndexedDB. Keep session identifiers in cookies marked HttpOnly, Secure and SameSite, which page scripts cannot read.

Related: localStorage in JavaScript, CORS explained, JSON formatter, Data URLs explained.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap