# Eviction and best-effort storage

> Which stored data a browser may delete unasked, the LRU order Chrome and Firefox use, Safari's seven-day rule, and why a bucket is cleared whole.

Every origin's storage bucket is best-effort until the browser grants persistence, and a
best-effort bucket may be deleted whole when the disk runs low, when the browser's overall ceiling
is reached, or, in Safari, when the user has not interacted with the site for seven days of
browser use. The unit of deletion is the bucket, so an evicted origin loses its caches, databases,
and service worker registration together.

## How it works

The Storage Standard defines two bucket modes, `best-effort` and `persistent`, and requires that
a bucket, when cleared, is cleared in its entirety ([Storage Standard](https://storage.spec.whatwg.org/),
whatwg.org). It leaves the trigger and the order to the browser, which is where the engines
diverge.

### Pressure-driven eviction in Chromium and Firefox

Chromium begins evicting when its overall ceiling, 80 % of total disk size, is exceeded, and
Firefox when the disk fills up. Both apply a least-recently-used order: the best-effort origin
visited longest ago is cleared first, then the next, until usage is back under the limit
([Storage quotas and eviction criteria](https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria),
developer.mozilla.org). An origin can therefore be evicted while far below its own quota: the
trigger is the combined total, and the selection is recency, not size. Persistent buckets are
skipped.

### Safari's seven-day rule

WebKit evicts on three triggers: exceeding the overall quota (80 % of disk for a browser app,
20 % for other apps that embed web content), system storage pressure, and Intelligent Tracking
Prevention ([Updates to Storage Policy](https://webkit.org/blog/14403/updates-to-storage-policy/),
webkit.org). The ITP trigger, introduced in iOS 13.4 and Safari 13.1, deletes an origin's
script-writable storage after seven days of Safari use without user interaction on the site.
The clock counts days on which Safari was used, not calendar days, and covers IndexedDB,
localStorage, sessionStorage, media keys, and service worker registrations and caches
([Full Third-Party Cookie Blocking and More](https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/),
webkit.org). An origin is excluded from eviction while it has an open page or while its bucket is
persistent.

Web apps added to the Home Screen run outside Safari and keep their own counter of days of
use, so an installed app that is opened resets its clock; Apple states it does not expect
first-party data in such an app to be deleted.

### Private browsing

Private windows are a separate lifecycle. Chrome's Incognito reduces the origin quota to about
5 % of disk and discards the data when the last Incognito window closes
([Storage for the web](https://web.dev/articles/storage-for-the-web), web.dev); Firefox and Safari
likewise clear private-session storage when the session ends. None of this is LRU eviction, and
`persist()` does not change it.

## Observed behaviour

Eviction leaves no event in the page. The only signals are indirect, and each one is checkable
without special tooling.

- `navigator.storage.estimate()` after eviction reports `usage` near zero for the origin while
  `quota` is unchanged, because the quota is a share of the disk and the bucket is simply empty.
- `caches.keys()` resolves to `[]` and `indexedDB.databases()` to `[]` (BCD
  `api.IDBFactory.databases`: Chrome 72, Firefox 126, Safari 14) for an origin whose bucket was
  cleared, which is how a service worker
  can detect that a precache it installed is gone and rebuild it on the next `activate`.
- Safari's seven-day deletion also removes the service worker registration, so a visit after the
  deadline starts with `navigator.serviceWorker.controller === null` and the `install` event runs
  again.

The check below runs on page load and distinguishes "evicted" from "first visit" by a marker in
`localStorage`, which Safari clears in the same sweep but Chromium and Firefox leave alone when
they evict the quota-managed bucket.

```js
async function detectEviction() {
  if (!('caches' in self) || !('storage' in navigator)) {
    return 'unknown'; // no quota-managed storage here: nothing to detect
  }
  const names = await caches.keys();
  const seenBefore = localStorage.getItem('shell-installed') === '1';
  if (names.length === 0 && seenBefore) {
    return 'evicted'; // bucket cleared under pressure; rebuild the precache
  }
  localStorage.setItem('shell-installed', '1');
  return names.length === 0 ? 'first-visit' : 'intact';
}
```

An `'evicted'` result is the moment to re-run the precache and, if the app has earned it, to
call `navigator.storage.persist()` so the next sweep skips this origin.

:::observed
Chrome DevTools, Application > Storage, reproduces the pressure case without a full disk: tick
**Simulate custom storage quota**, enter a figure below the current usage, and the next write
rejects with `QuotaExceededError` ([What's New in DevTools
(Chrome 88)](https://developer.chrome.com/blog/new-in-devtools-88), developer.chrome.com). The
override lowers only this origin's quota and does not trigger LRU eviction of other origins, so
it tests the write-failure path, not the deletion path.
:::

## See also

- [Storage Standard: buckets and eviction](https://storage.spec.whatwg.org/) (whatwg.org)
- [Updates to Storage Policy](https://webkit.org/blog/14403/updates-to-storage-policy/) (webkit.org)
- [Full Third-Party Cookie Blocking and More](https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/) (webkit.org)
- [StorageManager.persist() and persisted()](/reference/storage/persistence/)
- [StorageManager.estimate()](/reference/storage/quota-estimate/)
- [Cache Storage API](/reference/storage/cache-storage/)