Skip to content

Storage persistence, quotas, and eviction

In one line: Browsers give each origin a shared quota across Cache Storage, IndexedDB, and friends, then evict best-effort data first when the disk fills up. navigator.storage.estimate() tells you how much you’re using; persist() asks the browser to promote your origin to persistent, exempting it from automatic eviction.

  • Best-effort is the default. Data survives normally, but under storage pressure the browser may evict your entire origin without warning to reclaim space.
  • Persistent storage is opt-in via navigator.storage.persist(). A persistent origin is not auto-evicted — its data is only cleared when the user explicitly removes it.
  • Eviction is all-or-nothing per origin: the browser clears a whole origin’s box, not individual records. Best-effort origins are evicted first, least-recently-used among them.

A single per-origin quota is shared across the storage APIs:

  • Cache Storage (the offline app shell and assets a service worker caches)
  • IndexedDB (structured app data, large blobs)
  • Service worker registration scripts, plus legacy stores (localStorage, app-served files)

The quota is a fraction of free disk space, not a fixed number — it shrinks as the disk fills and varies by browser. Treat it as elastic and never assume a hard ceiling.

// How much you're using and your current ceiling (bytes).
const { usage, quota } = await navigator.storage.estimate();
// Is this origin already persistent?
const isPersisted = await navigator.storage.persisted();
// Ask to be promoted. Resolves true if granted.
if (!isPersisted) {
const granted = await navigator.storage.persist();
}

persist() returns a promise resolving to a boolean — never assume it succeeds. Re-check with persisted() rather than caching the result, since browser policy can change.

Browsers grant persistence based on signals that the user values the site — there is no single switch, and behavior differs across engines:

Signal Effect on the grant
PWA installed to the home screen / OS Strongest signal; persistence is typically granted automatically or on request.
Notifications permission granted Often enough on its own for the browser to grant persistence.
High site engagement (frequent visits, bookmarked) Heuristic that can tip the decision toward granting.
First-visit anonymous tab, low engagement Likely denied; request again later once engagement builds.
Firefox Prompts the user directly for a persistence permission.
  • Call persist() for any offline-critical PWA (offline drafts, queued mutations, cached media).
  • Request persistence after a meaningful action or install — not on first paint — so engagement signals exist.
  • Always handle a false result from persist() and degrade gracefully; do not block the UI on it.
  • Call estimate() before large writes; if usage is near quota, prune old caches first.
  • Wrap IndexedDB and Cache writes to catch QuotaExceededError, then evict stale data and retry.
  • Never treat the quota as fixed — re-estimate() rather than hardcoding a size budget.
  • Feature-detect navigator.storage before use; older browsers lack the StorageManager API.

Persistent storage is the difference between an installed PWA that reliably opens offline and one that silently loses the user’s queued data when the disk gets tight. Requesting it only after real engagement keeps the grant likely and the prompt non-annoying, while handling QuotaExceededError gracefully means a full disk degrades the experience instead of corrupting it — the durability users expect from something they chose to install.