Eviction and best-effort vs persistent storage
In one line: MDN describes every origin’s storage as either best-effort (the
default, evictable without warning under storage pressure) or persistent (opt-in via
navigator.storage.persist(), evicted only by explicit user action), and when a browser
does evict under pressure it works through best-effort origins in least-recently-used
order, clearing each evicted storage bucket in its entirety — in the common case, a bucket
corresponds to the whole origin, but a partitioned origin can have more than one bucket.
The two storage types
Section titled “The two storage types”- Best-effort is what every origin gets by default the moment it uses IndexedDB, Cache Storage, or similar APIs — MDN describes it as data that “persists as long as the origin is below its quota, the device has enough storage space, and the user doesn’t choose to delete the data.” No permission prompt is involved; it is simply the default.
- Persistent storage is granted only when an origin opts in and the browser agrees. Data in a persistent bucket “is only evicted, or deleted, if the user chooses to, by using their browser’s settings” — the browser will not silently clear it to reclaim space.
How eviction is ordered
Section titled “How eviction is ordered”When the browser needs to reclaim space, it applies a Least Recently Used (LRU) policy across best-effort origins: it deletes the least-recently-used origin’s data first, then moves to the next-least-recently-used origin, and continues “until the problem is resolved.” Persistent origins are skipped entirely by this process.
Some browsers additionally cap the total share of disk they will use at all — for example, Chrome documents using at most 80% of total disk size — so eviction of best-effort origins can begin even when no single origin has individually exceeded its own quota, simply because the combined total across all origins is too large.
Eviction is all-or-nothing per storage bucket
Section titled “Eviction is all-or-nothing per storage bucket”The Storage Standard requires that whenever a storage bucket is cleared, it “must be cleared in its entirety” — a browser does not selectively drop some IndexedDB records or some cache entries to free a smaller amount of space. MDN says browsers manage stored data per origin in most cases, but may further separate a single origin’s data into different partitions — for example when that origin is loaded in an iframe under multiple different third-party origins — so the unit cleared at once is the bucket, which in the common case is the whole origin, but a partitioned origin can lose one bucket while another survives. Either way, treating any best-effort data as a durable source of truth is risky regardless of how small it is.
Private browsing
Section titled “Private browsing”Private/incognito browsing sessions are a separate case: MDN notes that in these modes “browsers may apply different quotas, and stored data is usually deleted when the private session ends” — a distinct lifecycle from the pressure-triggered LRU eviction described above, driven by the session ending rather than by disk space.
Where it is supported
Section titled “Where it is supported”The exact signals a browser weighs before granting persistence are not standardized and
vary by engine — MDN notes that Firefox notifies the user with a UI popup requesting
permission, while Safari and most Chromium-based browsers automatically approve or deny the
request based on the user’s history of interaction with the site, without showing a prompt.
Always feature-detect navigator.storage rather than assuming it is present, and re-check
persisted() rather than caching an old result, since policy can change between visits.
| Browser / Platform | Support | Since | Confidence | Source | Notes |
|---|---|---|---|---|---|
| Chrome (Desktop) | ✅ yes | 55 | high | ref | — |
| Chrome (Android) | ✅ yes | 55 | high | ref | — |
| Edge (Desktop) | ✅ yes | 79 | high | ref | — |
| Firefox (Desktop) | ✅ yes | 57 | high | ref | — |
| Firefox (Android) | ✅ yes | 57 | high | ref | — |
| Safari (macOS) | ✅ yes | 15.2 | high | ref | — |
| Safari (iOS) | ✅ yes | 15.2 | high | ref | — |
| Samsung Internet | ✅ yes | 6.0 | medium | ref | — |
See /compatibility/ for current per-browser data.
Checking and requesting persistence
Section titled “Checking and requesting persistence”const isPersisted = await navigator.storage.persisted();if (!isPersisted) { const granted = await navigator.storage.persist(); // granted === true only if the browser promoted this origin; never assume success.}Detecting eviction risk and providing a fallback
Section titled “Detecting eviction risk and providing a fallback”persisted() reports only whether an origin’s storage is currently in the persistent
mode — it does not confirm that any particular cached item still exists, and a persistent
origin’s data can still be cleared by explicit user action. Code that must not silently
lose data should check for the cached value being present, and fall back to re-fetching or
re-deriving it on a best-effort origin where eviction can also happen without warning:
async function loadCriticalData(fetchFresh) { const persisted = 'storage' in navigator && (await navigator.storage.persisted()); const cached = persisted ? await readFromLocalCache() : null; if (cached != null) { return cached; } // Best-effort data may have been cleared under storage pressure, and even a // persistent origin's cache entry may simply be missing — re-derive it either way. return fetchFresh();}What goes wrong
Section titled “What goes wrong”- Treating best-effort storage as durable. It is the default for IndexedDB and Cache
Storage; without calling
persist(), any of it can be cleared without warning. - Assuming eviction is partial. It never is — an evicted storage bucket is cleared in its entirety, not just the oldest records within it, and that bucket is usually the whole origin but can be a narrower partition.
- Ignoring the combined-disk ceiling. Some browsers evict best-effort origins once total usage across all origins passes a disk-wide threshold, even if your origin alone is well under its own quota.
- Expecting
persist()to always succeed. It resolves to a boolean; afalseresult means the origin stays best-effort and can still be evicted. - Relying on private-browsing storage across sessions. It follows a different, shorter lifecycle tied to the session, not the LRU pressure eviction described here.
Practical checklist
Section titled “Practical checklist”- Call
navigator.storage.persist()for any origin whose offline data must survive disk pressure, and check the resolved boolean rather than assuming success. - Never assume a best-effort origin’s IndexedDB or Cache Storage contents are still present — re-check or re-fetch before treating them as a source of truth.
- Remember eviction is all-or-nothing per storage bucket: design around losing a whole bucket at once (usually the entire origin, sometimes a narrower partition), not individual records.
- Don’t rely on data written during a private/incognito session outliving that session.
- Feature-detect
'storage' in navigatorbefore callingpersist()orpersisted().
Where to go next
Section titled “Where to go next”- Storage persistence, quotas, and eviction — how to request persistence and read quota usage with the StorageManager API.
- Storage quota and estimate — using
navigator.storage.estimate()to measure usage against quota.