The browser gives you the Cache API (the global `caches` / `CacheStorage`) alongside the HTTP cache it manages itself. How do the two differ, and what does that difference mean for code that stores responses?
answer
- one store you own, one you do not
- headers drive the browser's, code drives yours
- no expiry semantics at all
- precaching still goes through the network stack
basics
~20 sThe Cache API is a script-owned store of Request/Response pairs that you fill, look up and delete explicitly, with no expiry of its own. The HTTP cache is browser-managed, driven by response headers, and unreachable from JavaScript.
solid answer
~50 sThe HTTP cache is the browser's own store: it decides what to keep based on the response headers, reuses entries automatically for matching requests, and gives script no way to enumerate, read or delete an entry. The Cache API is the opposite — `caches.open(name)` gives you a `Cache`, a keyed collection of `Request` → `Response` pairs that only grows when your code calls `put`, `add` or `addAll`, and is only read when your code calls `match` and returns the result (usually from a service worker's `fetch` handler via `event.respondWith`). A Cache entry has no freshness: `Cache-Control: max-age` on a stored response means nothing to it, so eviction is entirely your responsibility. The two still interact — `cache.add()` does a real `fetch`, which can be answered from the HTTP cache, so precaching should use `new Request(url, { cache: 'reload' })` to force the network.
code
javascript · 10 linesasync function precache() {
const cache = await caches.open('static-v3');
// cache: 'reload' bypasses the HTTP cache so we store fresh bytes
await cache.addAll(
['/app.js', '/app.css'].map((url) => new Request(url, { cache: 'reload' }))
);
const hit = await cache.match('/app.js');
console.log(hit ? await hit.text() : 'miss');
}
precache();go deeper
Know that caches.open(name) gives you a store of Request/Response pairs your code fills and reads, and that it is a different thing from the cache the browser manages by itself.
Be ready to explain that the Cache API ignores freshness headers entirely, that entries live until deleted, and that cache.add() performs a normal fetch which the HTTP cache can answer.
Show you design invalidation deliberately: versioned cache names, cache: 'reload' on precache requests, and an explicit story for how a bad entry gets removed from real users' devices.
Own the split: which assets get header-driven caching and which get pinned by your code, what the rollback path is when a poisoned entry ships, and how much origin storage the strategy is allowed to consume.
## Two different caches, one browser A browser holds responses in more than one place, and the two that matter here are owned by different parties. The **HTTP cache** is the browser's own. When a response arrives, the browser may store it and reuse it automatically for later requests, based on the headers the server sent. Your JavaScript never sees that store: there is no API to list its entries, read one, or remove one, and the browser may drop entries whenever it wants. Its behaviour is a property of the network stack, not of your application code. The **Cache API** is a separate, script-owned store. The entry point is the global `caches`, a `CacheStorage` object available in windows, dedicated workers and service workers, only in **secure contexts** (HTTPS, or `localhost` during development), and partitioned per origin. `caches.open('v1')` resolves with a `Cache`: a keyed collection whose keys are `Request` objects and whose values are `Response` objects. `caches.keys()` lists the cache names, and `caches.delete(name)` removes a whole named cache. ## What "script-owned" actually changes **Population is explicit.** Nothing lands in a `Cache` unless you call `cache.put(request, response)`, `cache.add(request)` or `cache.addAll(list)`. A response the browser happened to download does not appear there. **Reads are explicit.** `cache.match(request)` resolves with a `Response` or `undefined`; `caches.match(request)` searches every cache of the origin. A stored response is only *served* to the page when your code hands it back — in a service worker, `event.respondWith(cached)`. There is no automatic interception. **There is no freshness model.** This is the part people get wrong most often. The Cache API does not interpret `Cache-Control`, `Expires`, `Age` or `ETag`. An entry stored with `max-age=60` is still returned by `match()` a month later, byte for byte. The only response header consulted during lookup is `Vary`, and only to decide whether a stored entry matches the incoming request. Consequently, invalidation is a feature you write: `cache.delete(request)` for one entry, `caches.delete(name)` for a whole generation. **Lifetime is long.** Entries survive reloads, tab closes and browser restarts. They disappear when your code deletes them, when the user clears site data, or when the browser evicts the origin's storage under pressure. ## They are not isolated from each other `cache.add(url)` and `cache.addAll(urls)` are conveniences that perform a real `fetch` and store the result. That fetch travels the normal network stack, so it can be answered *from the HTTP cache* — which means a precache step can quietly store a stale copy of a file you just deployed. The fix is to control the request's cache mode: ```js const cache = await caches.open('static-v3'); await cache.addAll( ['/app.js', '/app.css'].map((u) => new Request(u, { cache: 'reload' })) ); ``` `cache: 'reload'` makes the fetch bypass the HTTP cache on the way out and update it on the way back. `'no-store'` avoids touching it in either direction. ## Choosing between them If the rule you want is "reuse this for ten minutes, then revalidate", that is a header-driven policy and the HTTP cache implements it for free. Reach for the Cache API when you need something the HTTP cache structurally cannot give you: guaranteed availability offline, an atomic set of files pinned together for one app version, a response you assembled yourself, or a lookup that must succeed *now* with no network round trip and no dependence on the browser's eviction whims. ## Common misreadings - "The Cache API respects `Cache-Control`." It does not. Freshness is yours to model, e.g. by storing a timestamp alongside the entry or by versioning the cache name. - "Writing to a Cache disables the HTTP cache." The two are independent; a fetch still consults the HTTP cache regardless of what you stored. - "`caches` only exists in a service worker." It exists in any secure-context window or worker; a page can precache before ever registering a service worker. - "Cache entries expire on their own." They do not expire; whole-origin eviction under storage pressure is a different mechanism and is not per-entry expiry.
- If a Cache entry never expires, how do you stop an application from serving last month's JavaScript bundle forever?Version the cache name and treat a generation as immutable: write `static-v4`, populate it fully, then delete `static-v3`. For data that ages rather than ships, store your own timestamp next to the payload — or in a separate IndexedDB record — and treat the entry as stale in your own read path. The API will never do it for you.
- Can you put a response you built in JavaScript into a Cache, or only ones that came from the network?Any `Response` works, including one you constructed: `cache.put('/offline.html', new Response(html, { headers: { 'Content-Type': 'text/html' } }))`. That is a common way to pin an offline fallback page without an extra network round trip. The key must still be a GET request with an http or https URL.
- Is the Cache API available on a plain HTTP page?No. `caches` is exposed only in secure contexts — HTTPS, plus `localhost` and similar trustworthy origins for development. On a plain HTTP origin the global is absent, so feature-detect with `'caches' in window` rather than assuming it exists.
saying these in an interview costs you the question
- Says Cache API entries expire when Cache-Control max-age elapses
- Thinks caches only exists inside a service worker
- Believes a cached Response is served automatically without respondWith
- Claims writing to the Cache API disables the HTTP cache
- Assumes cache.addAll always hits the network