10LOC
Web Platformintermediate

Persist storage with navigator.storage.persist

Published August 20, 2027

export const ensurePersistentStorage = async (): Promise<boolean> => {
  if (!navigator.storage?.persist) return false;
  if (await navigator.storage.persisted()) return true;
  return navigator.storage.persist();
};

export const getStorageHeadroom = async (): Promise<{ usedBytes: number; quotaBytes: number }> => {
  const { usage = 0, quota = 0 } = await navigator.storage.estimate();
  return { usedBytes: usage, quotaBytes: quota };
};

What

navigator.storage.persist() asks the browser to mark the current origin's storage (IndexedDB, Cache API, OPFS, localStorage) as persistent, so it's exempt from the automatic eviction browsers use to reclaim disk space under pressure.

Why it matters

Without persistence, storage lives in "best-effort" mode: if the device runs low on disk space, the browser can silently delete an entire origin's storage — usually the least-recently-used one — with no warning to the user and no event your app can catch beforehand. For an app that keeps meaningful state client-side (an offline-first note-taking app, a local-first editor, cached media for offline playback), that's data loss the user never agreed to. persist() moves an origin out of that eviction pool; once granted, the browser will only clear it if the user does so explicitly (clearing site data, uninstalling the app).

How it works

  • ensurePersistentStorage first checks navigator.storage?.persist exists — Web Workers don't expose persist() (only persisted()), so this doubles as an environment check.
  • It checks persisted() before calling persist() — asking again when storage is already persistent is harmless but wasteful, and on some browsers repeated calls can re-trigger evaluation logic.
  • getStorageHeadroom calls estimate(), which returns usage and quota in bytes; both are optional in the type because a handful of edge cases (opaque origins, storage disabled) omit them, hence the destructuring defaults.

Gotchas

  • Chrome auto-decides, silently. It grants or denies persist() based on a site engagement heuristic — visit frequency, whether the site is installed or bookmarked, notification permission — with no prompt shown to the user either way. A fresh, rarely-visited origin will likely get false back on the first call.
  • Firefox asks the user directly, via a permission popup, the first time a site calls persist().
  • Safari has supported persist()/persisted() since Safari 15.2, but doesn't publicly document its granting heuristic the way Chrome does — treat it as opaque and test the actual return value rather than assuming.
  • A false return isn't an error — it means "keep working, but your storage can still be evicted." Design for that outcome (e.g. periodic export, sync-to-server) rather than treating persistence as guaranteed.
  • estimate().quota is a ceiling shared across all storage APIs for the origin combined (IndexedDB + Cache API + OPFS, etc.), not a separate budget per API.

Related

  • Cache API / IndexedDB — the storage this actually protects; persist() changes eviction behavior, it doesn't create or manage storage itself.
  • Storage Buckets API — a newer, more granular alternative that lets you set persistence and eviction policy per named bucket instead of for the whole origin at once.