skip to content

Quotas, Eviction, and Persistence

You will learn that client storage is a loan, not a guarantee: quotas are per-origin, best-effort data gets evicted, and persistence must be requested. Interviewers ask this to see whether your offline design survives disk pressure.

on this pageshow

questions

5

What does `navigator.storage.persist()` do, and what does a `true` result actually guarantee about the data your origin has stored?

level: middleimportance: must knowfreq 48%

answer

  1. default mode is a loan
  2. one boolean comes back
  3. one browser prompts, another decides quietly
  4. exemption from eviction only
  5. quota unchanged, user still wins

basics

~20 s

navigator.storage.persist() asks the browser to move the origin from best-effort to persistent storage. A true result means the browser will not evict that data automatically under disk pressure. The user can still clear it, and the origin's quota does not change.

solid answer

~50 s

Storage is best-effort by default, meaning the browser may throw it away when the device runs short of space. `navigator.storage.persist()` requests the `persistent-storage` permission and resolves to a boolean: `true` means the origin's data is now exempt from automatic eviction. The two big caveats are what it does not buy you. It does not raise the quota — persistence changes eviction policy, not the ceiling — and it does not defend against the user, who can still clear site data from browser settings or a site-issued `Clear-Site-Data` response. How the decision is made also differs: Firefox shows the user a prompt, while Chromium never prompts and instead decides silently from signals like installation, notification permission or site engagement, so a call can simply return `false` with nothing shown. Use `navigator.storage.persisted()` to read the current state without asking.

code

javascript · 9 lines
javascript
async function ensurePersistence() {
  if (!navigator.storage?.persisted) return 'unsupported';
  if (await navigator.storage.persisted()) return 'already-persistent';

  const granted = await navigator.storage.persist();
  return granted ? 'granted' : 'best-effort';
}

ensurePersistence().then((mode) => console.log('storage mode:', mode));

go deeper

for a junior

Know that client storage is best-effort by default, that navigator.storage.persist() asks to upgrade it, and that the call resolves to a boolean rather than throwing when refused.

for a middle

Be ready to explain that persistence exempts an origin from automatic eviction only, that it leaves quota untouched, and that Firefox prompts while Chromium decides from engagement signals with no UI.

for a senior

Show judgment about when to ask: tie the request to a user action that justifies it, and keep a working fallback because most sessions will run best-effort whatever you request.

for a principal

Own the durability contract. Decide which data may live only on the client, what the recovery path is when it disappears, and how the product earns the automatic grant instead of depending on it.

## Two storage modes The Storage Standard gives every origin's storage one of two modes. **Best-effort** is the default: the browser is free to delete the data when it needs the space back, and it does so without asking and without notifying the page. **Persistent** means the browser undertakes not to evict that data automatically; it stays until the user or the site removes it. This is the single most important framing for client storage. Writing to IndexedDB is not the same as writing to disk in a native app. By default you have a loan, and persistence is the request to upgrade that loan. ## Requesting it ```js if (navigator.storage?.persist) { const granted = await navigator.storage.persist(); console.log(granted ? 'persistent' : 'best-effort'); } ``` `persist()` returns a promise resolving to a boolean. It is a `StorageManager` method, so it requires a secure context, and it applies to the origin's storage as a whole rather than to one database or one cache. Under the hood it is a request for the `persistent-storage` permission, which means it can also be inspected through the Permissions API: ```js const status = await navigator.permissions.query({ name: 'persistent-storage' }); // status.state is 'granted', 'denied' or 'prompt' ``` To find out where you already stand without requesting anything, call `navigator.storage.persisted()`, which resolves to a boolean describing the current mode. ## Browsers decide very differently This is where candidates usually get caught out, because the API looks like one thing and behaves like two. **Firefox treats it as a user permission.** Calling `persist()` shows a prompt asking whether the site may store data persistently, and the promise resolves according to what the user chooses. Because it is a real prompt, it belongs behind a deliberate user action — offering to make a document available offline, for example — not on page load. **Chromium never prompts.** It decides automatically from engagement-style signals: whether the site is installed as an application, whether it has been granted the notifications permission, whether the user has bookmarked it, and how much site engagement it has accumulated. If none of the signals is present, `persist()` simply resolves `false` and the user sees nothing at all. That has a design consequence: you cannot treat a `false` as "the user said no" and you cannot fix it by asking again. You earn it, or you design for going without it. Safari implements the API too, but WebKit layers its own tracking-protection rules on top of storage lifetime, so persistence should not be assumed to behave identically there. ## What `true` does and does not buy you Granted persistence buys exactly one thing: exemption from automatic eviction when the device is under storage pressure. Everything else stays as it was. - **Quota is unchanged.** Persistence is about eviction policy, not size. A persistent origin gets the same disk-derived ceiling, and writing past it still fails. - **The user still wins.** Clearing browsing data, clearing site data for that origin, or removing the site from browser settings deletes persistent storage like any other. A `Clear-Site-Data: "storage"` response header from your own server does the same. - **It is not backup.** The data lives in one browser profile on one device. A reinstall, a new profile, a second browser or a second machine all start empty. - **It does not survive an uninstall** of the browser or a profile wipe. So the honest summary in an interview is: persistence removes one deletion path — the silent, involuntary one — and leaves every other path intact. ## When to ask, and what to do with the answer Ask when the client genuinely holds something the server cannot regenerate: a document being edited offline, a queue of unsynced changes, a large downloaded corpus the user explicitly chose to keep. Ask at the moment that makes the reason obvious to a user, since one browser will show them a prompt. Do not ask reflexively on startup for data that is merely a cache of server state — you will be denied in Chromium, you will burn a prompt in Firefox, and the data was replaceable anyway. Whatever the answer, keep the fallback path. Code that only works when `persist()` returned `true` is code that breaks for most users, because most users have not triggered the automatic grant. Detect an empty store on startup and re-hydrate; sync unsynced work as soon as a network is available rather than trusting local durability. ## A note on buckets The Storage Standard also describes storage buckets, which let a site split its storage into named units and mark individual buckets persistent, so the important bucket can outlive the disposable one. This is available only in Chromium-based browsers, so treat it as a progressive enhancement rather than a design foundation.

  • If persist() resolves false in Chromium, what should the app do next?
    Nothing retry-shaped. Chromium showed no prompt and no user said no, so calling again changes nothing; the grant follows engagement signals such as installation or notification permission. Treat false as the normal case: keep client data replaceable, re-hydrate from the server when the store is empty, and flush unsynced work to the network promptly rather than relying on local durability.
  • How do you check whether an origin already has persistent storage without triggering a prompt?
    Call navigator.storage.persisted(), which resolves to a boolean describing the current mode without requesting anything. You can also inspect navigator.permissions.query({ name: 'persistent-storage' }) and read state, which returns 'granted', 'denied' or 'prompt'. Both are safe to call on startup; persist() is the only call that may show UI.
  • Does persistent storage give the origin a bigger quota?
    No. Persistence and quota are independent. The quota is still derived from the device's disk and is unchanged by the grant, so a persistent origin can still exhaust its budget and see writes rejected. What changes is that the browser will not silently evict the data when the device is short of space.
  • Is persistent storage a substitute for syncing data to a server?
    No. It removes only the automatic-eviction path. The user can still clear site data, a Clear-Site-Data response can wipe it, and the data exists in exactly one browser profile on one device — so a reinstall, a new profile or a second machine starts empty. Anything the user cannot afford to lose still needs a server round trip.

saying these in an interview costs you the question

  • Thinks persist() raises the storage quota
  • Claims persistent data survives the user clearing site data
  • Expects a permission prompt in every browser
  • Retries persist() in a loop after it returns false
  • Treats persistent storage as a backup or sync mechanism

context

open as a page

What does `navigator.storage.estimate()` report in a browser, and what does the `quota` value in its result actually represent?

level: juniorimportance: should knowfreq 42%

basics

~20 s

navigator.storage.estimate() resolves with usage and quota in bytes: roughly what the origin already stores, and the approximate ceiling it may reach. Both numbers are deliberately imprecise, and they cover all of the origin's quota-managed storage as one shared pool.

open as a page

A web app that writes large blobs to client storage sees the writes fail for some users. Which error does a browser raise when an origin's storage quota is exhausted, and what determines how much room a given user actually has?

level: middleimportance: should knowfreq 44%

basics

~20 s

The browser raises a QuotaExceededError DOMException: thrown synchronously by localStorage.setItem, aborting the transaction in IndexedDB, rejecting the promise in the Cache API. The ceiling is derived from the device's free disk space, so it varies per machine and shrinks as the disk fills.

open as a page

When a device runs low on disk space, how does a browser evict a best-effort origin's stored data, and what does that granularity mean for an offline-capable web app?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Browsers evict a whole best-effort origin at once, least-recently-used first: its databases, caches and web storage all go together, never just the oldest records inside one store. Offline apps must therefore treat client storage as a cache that can be empty on the next visit.

open as a page

A widget served from widget.example is loaded in an iframe on two different top-level sites. Modern browsers no longer let it read the same client-side storage in both. What changed, and what is the unit that quota and eviction now apply to?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Browsers now key client storage on the top-level site plus the frame's origin, not the origin alone. An embedded widget therefore gets a separate store, a separate quota and a separate eviction fate on every site that embeds it, and cannot carry state between embedders.

open as a page