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 vs persistent storage
Section titled “Best-effort vs persistent storage”- 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.
What counts toward quota
Section titled “What counts toward quota”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.
Estimating and requesting persistence
Section titled “Estimating and requesting persistence”// 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.
How browsers decide to grant persistence
Section titled “How browsers decide to grant persistence”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. |
Practical checklist
Section titled “Practical checklist”- 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
falseresult frompersist()and degrade gracefully; do not block the UI on it. - Call
estimate()before large writes; ifusageis nearquota, 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.storagebefore use; older browsers lack the StorageManager API.
What this means for trust
Section titled “What this means for trust”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.