Skip to content

The Cache Storage API

The Cache Storage API is exposed as the caches global inside a service worker context. To open (or create) a named cache, call caches.open('cache-v1'), which returns a Promise<Cache>. Caches are identified purely by string name — there is no schema, no expiry, and no automatic eviction. You own the lifecycle entirely.

self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('cache-v1').then((cache) => {
console.log('Cache opened:', cache);
})
);
});

Multiple caches can coexist. A common pattern is to keep one cache per version of your app so that an old SW and a new SW never share the same cache namespace.

cache.addAll(urls) accepts an array of URL strings and fetches and stores all of them atomically. If any single request fails — network error, 404, or otherwise — the entire addAll call rejects and the install event fails. This is intentional: it guarantees your app shell is either fully cached or not cached at all.

const CACHE_NAME = 'cache-v1';
const APP_SHELL = [
'/',
'/index.html',
'/app.js',
'/style.css',
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL))
);
});

Keep the precache list short. Every URL in the list is a mandatory network request at install time — a 404 will prevent the SW from activating.

For runtime caching you add individual responses using cache.put(request, response). You retrieve them with either cache.match(request) (searches only that specific cache) or caches.match(request) (searches all caches and returns the first match).

// Runtime cache: intercept a fetch, cache the response, return it
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) return cached;
return fetch(event.request).then((response) => {
// Store a clone; return the original
caches.open('cache-v1').then((cache) => {
cache.put(event.request, response.clone());
});
return response;
});
})
);
});
// Searching a specific named cache vs. all caches
caches.open('cache-v1').then((cache) => {
cache.match('/app.js').then((res) => {
console.log('From named cache:', res);
});
});
caches.match('/app.js').then((res) => {
console.log('From any cache:', res);
});

A Response object wraps a body that is a one-time readable stream. Once you consume it — by calling .json(), .text(), or passing it to cache.put() — the stream is drained and the body is gone. Any subsequent read returns an empty result.

When you want to both cache a response and return it to the caller, you must clone it first. response.clone() creates a second Response that shares the same body data but has an independent read cursor.

fetch(event.request).then((response) => {
const clone = response.clone(); // independent copy
cache.put(event.request, clone); // cache the clone
return response; // return the original to the page
});

When you deploy a new version of your SW with a new cache name, the old caches remain on disk until you explicitly remove them. The activate event is the right place to do this: the new SW has taken over, so it is safe to delete caches that the old SW depended on.

const CACHE_VERSION = 'v1';
const CURRENT_CACHES = new Set(['cache-v1', 'images-v1']);
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => !CURRENT_CACHES.has(name))
.map((name) => {
console.log('Deleting old cache:', name);
return caches.delete(name);
})
);
})
);
});

After cleanup, call self.clients.claim() if you want the newly activated SW to take control of existing open pages immediately rather than waiting for a full page reload.

Runs a real service worker + manifest in your browser.
Which Cache Storage method atomically stores an array of URLs at install time?
Why must you clone a Response before passing it to cache.put()?
What does caches.match() do differently from cache.match()?
When should old caches be deleted?