The Cache API Guide
By Flavio Copes
Learn how the browser Cache API stores Request and Response pairs, how to match and update entries, and which expiration and quota limits matter.
The Cache API stores Request and Response pairs under your site’s origin.
It is commonly used by service workers to make pages work offline. You can also use it from a window or another worker.
The API is available in secure contexts, which means HTTPS in production. Local development origins such as http://localhost are treated as trustworthy.
The Cache API is not the HTTP cache
Cache API entries are separate from the browser’s normal HTTP cache.
The API does not automatically follow HTTP cache expiration headers. An entry remains until your code removes or replaces it, or the browser evicts site data.
Use the Cache API for URL-addressable resources such as pages, scripts, images, and API responses. Use IndexedDB for application records that are not naturally represented by requests and responses.
Open a named cache
The global caches object is a CacheStorage.
Open a cache with caches.open():
const cache = await caches.open('assets-v1')
If the named cache does not exist, the browser creates it.
Give caches versioned names. When the contents change in an incompatible way, open a new version and remove the old one.
Add resources
cache.add() fetches one resource and stores the response:
const cache = await caches.open('assets-v1')
await cache.add('/styles.css')
add() rejects if the fetch fails or returns a response outside the successful 200–299 range.
Add several resources with addAll():
await cache.addAll([
'/',
'/styles.css',
'/app.js',
])
If one request fails, addAll() rejects. Handle the error so a failed install does not look successful.
Store a fetched response with put
Use cache.put() when you need to inspect or transform the response first:
const request = new Request('/api/posts')
const response = await fetch(request)
if (response.ok) {
const cache = await caches.open('api-v1')
await cache.put(request, response)
}
Unlike add(), put() can store error responses if you pass one. Check the response deliberately.
Request and response bodies are streams. If you need to return a response and cache it, clone it first:
const response = await fetch(request)
if (response.ok) {
await cache.put(request, response.clone())
}
return response
The Cache API only stores GET requests. It is not a general store for POST request results.
Match a cached response
Use cache.match():
const cache = await caches.open('api-v1')
const response = await cache.match('/api/posts')
if (response) {
const posts = await response.json()
console.log(posts)
}
It resolves to the first matching Response, or undefined when no entry matches.
Matching normally considers the request URL, method, and relevant Vary response headers. Options such as ignoreSearch and ignoreVary can loosen matching, but use them carefully. They can return a response created for different input.
Search every named cache with caches.match():
const response = await caches.match('/styles.css')
This returns the first match in cache creation order. Opening the intended named cache is clearer when cache order should not decide the result.
Build a cache-first service worker
A service worker can answer a request from the cache and fall back to the network:
self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET') return
event.respondWith(
caches.match(event.request).then(async (cachedResponse) => {
if (cachedResponse) {
return cachedResponse
}
const response = await fetch(event.request)
if (response.ok) {
const cache = await caches.open('runtime-v1')
await cache.put(event.request, response.clone())
}
return response
}),
)
})
This is a basic cache-first strategy. It can serve stale data forever unless you add an update and deletion policy.
Different resources need different strategies. Static versioned assets suit cache-first. Frequently changing API data may need network-first or stale-while-revalidate behavior.
CORS and opaque responses
The Cache API does not bypass CORS.
A no-cors fetch can produce an opaque response. Your code cannot inspect its status, headers, or body. cache.put() can store it, but cache.add() rejects it because an opaque response does not report a successful status.
Cache opaque responses only when you trust the URL and understand the storage and failure tradeoffs.
List and remove entries
List the requests stored in one cache:
const cache = await caches.open('assets-v1')
const requests = await cache.keys()
for (const request of requests) {
console.log(request.url)
}
Delete one matching entry:
await cache.delete('/old-styles.css')
List every named cache:
const names = await caches.keys()
Delete a complete named cache:
await caches.delete('assets-v1')
Delete old cache versions
Service workers commonly remove old versions during activation:
const currentCaches = new Set([
'assets-v2',
'runtime-v1',
])
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((names) => {
return Promise.all(
names
.filter((name) => !currentCaches.has(name))
.map((name) => caches.delete(name)),
)
}),
)
})
Only delete cache names owned by this application. Other code on the same origin shares the same CacheStorage.
Storage can be evicted
Cache storage uses the site’s browser storage quota. Limits and eviction rules vary by browser, device, free disk space, and browsing mode.
Check estimated usage with the Storage API:
const estimate = await navigator.storage.estimate()
console.log(estimate.usage)
console.log(estimate.quota)
Do not treat a cache as the only copy of important user data. The browser can evict it, and users can clear it.
See the MDN references for Cache, CacheStorage, and service worker caching.
Related posts about platform: