How Browsers Evict Cached Resources Under Storage Pressure

Problem Statement

A site ships fingerprinted assets with a one-year max-age and immutable, and repeat-visit measurements still show large transfers. Nothing is wrong with the headers. The browser discarded the entries because the disk came under pressure, and no directive can prevent that.

Prerequisite Concepts

Step-by-Step Resolution

Step 1 — Understand the quota model

Browsers do not allocate a fixed cache size per site. They maintain a pool sized as a fraction of free disk and divide it across origins on demand. Chromium exposes an estimate through the Storage API:

const { usage, quota } = await navigator.storage.estimate();
console.log(`${(usage / 1e6).toFixed(1)} MB used of ${(quota / 1e6).toFixed(0)} MB`);

Two properties of the model matter. The quota is a share of free disk, so it shrinks as the user’s disk fills — a laptop at 98% capacity offers a fraction of what the same machine offered a month earlier. And eviction operates at origin granularity: under pressure the browser discards whole origins, least recently used first, rather than picking individual entries.

From disk pressure to a cold cacheThe browser's quota tracks free disk, and when the pool must shrink it discards whole origins in least-recently-visited order rather than individual entries.Disk fillsfree space dropsuser activityPool shrinksquota recalculatedpressureLRU by originoldest visit firstselectedYour entriesall discarded together
Eviction happens per origin, not per entry

Step 2 — Measure what actually survives

Open a fresh profile, load the page, close and reopen the browser, and load it again. Record the transferred bytes on the second load:

# A rough server-side proxy for the same measurement: count requests that
# arrive with a conditional header versus those that arrive without one
awk '$0 ~ /If-None-Match|If-Modified-Since/ {c++} END {print c" conditional requests"}' access.log

A repeat visit that transfers close to zero bytes means the cache survived. A repeat visit transferring most of the page weight means it did not, and the headers are not the reason.

Step 3 — Reproduce it deliberately

Eviction is difficult to observe accidentally and easy to observe on purpose. Fill the profile’s allocation with a large synthetic store, then check which entries remain:

// Consume quota until the browser refuses, then observe what was discarded
const cache = await caches.open('pressure-test');
const blob = new Blob([new Uint8Array(8 * 1024 * 1024)]);
try {
  for (let i = 0; i < 4000; i++) {
    await cache.put(`/pressure/${i}`, new Response(blob));
  }
} catch (e) {
  console.log('quota reached at iteration', e.name);
}

Running this on a test profile and then reloading the site shows exactly how the browser prioritises — and confirms that a long max-age provides no protection.

What survives eviction and what does notRetention is decided by the browser's storage policy, not by response directives, and only origin-controlled persistent storage can opt out.Survives routine evictionUnder site controlHTTP cache entryimmutable assetCache Storage, defaultCache Storage, persistedusuallyIndexedDB, persistedusually
Directives do not influence retention; storage type does

Step 4 — Move the assumption to a tier you control

If repeat-visit performance matters, the durable answer is to stop depending on browser retention. The edge is a tier where retention can be measured, sized and purged. A cold browser fetching from a warm edge costs one round trip and no origin work — which is a very different cost from a cold browser fetching from the origin.

Two adjustments follow. First, ensure every asset carries a validator so an evicted-but-unchanged entry can be restored with a 304 rather than a full transfer where the browser retained the metadata. Second, keep the critical path small enough that a fully cold load is acceptable, because for a meaningful share of visits that is exactly what happens.

Repeat-visit transfer by cache stateThe difference between a warm browser cache and a cold one is large, but a warm edge keeps even the cold case far below a true origin fetch.Warm browser cache4KB conditional requests onlyCold browser, warm edge810KB one round trip per assetCold browser, cold edge810KB plus origin latencythe byte count is the same; the latency and origin cost are not
Bytes transferred on a repeat visit, by cache state

Expected Output / Verification

A healthy repeat visit shows almost no transfer and a handful of conditional requests. An evicted visit shows the full asset set refetched with 200 responses and no If-None-Match headers at all — the absence of the conditional header is the distinguishing signal, because a stale-but-present entry would have sent one.

# Retained entry, revalidated
GET /assets/app.b7f31e2a.js
If-None-Match: "b7f31e2a"
-> 304 Not Modified

# Evicted entry, refetched
GET /assets/app.b7f31e2a.js
-> 200 OK, 184 KB

Edge Cases

  • Private browsing. Most browsers keep the HTTP cache in memory only, so it is discarded on session end regardless of quota. Performance measured in a private window systematically understates repeat-visit performance.
  • Storage cleared by the user or by a cleanup tool. Common enough to be worth designing for, and indistinguishable from eviction at the server.
  • Very large single assets. Some browsers decline to store responses above a fraction of the origin quota, so an unusually large bundle may never be cached at all while smaller siblings are.
  • Origin sharding. Splitting assets across several hostnames splits the quota allocation too, and each origin is evicted independently — one more reason sharding is a net loss on modern connections.

Frequently Asked Questions

Can a site reserve browser cache space?

Not for the HTTP cache. The Storage API offers persistent storage for origin-controlled stores such as Cache Storage and IndexedDB, which exempts them from routine eviction, but the HTTP cache populated by ordinary responses has no equivalent reservation.

Does immutable protect an entry from eviction?

No. immutable suppresses revalidation while the entry exists; it does not influence retention. An evicted immutable asset is refetched in full exactly like any other.

How much space does a browser actually give an origin?

It varies by browser, version and available disk. Chromium allows a single origin a large share of a pool sized as a fraction of free disk, but the pool shrinks as the disk fills and whole origins are evicted least-recently-used. Treat the number as unknowable and design for a cold cache.

Is a service worker cache safer than the HTTP cache?

Somewhat. Cache Storage is subject to the same quota but can be marked persistent, and its contents are under the site’s control rather than the browser’s heuristics. It is a reasonable place for a small, deliberately chosen set of assets, not a substitute for HTTP caching.


Back to Cache Storage, Eviction and Capacity