# Caching strategies for service workers

> What each of the five fetch-handler strategies (Cache First, Network First, Stale-While-Revalidate, Network Only, Cache Only) returns, what it costs, the response-cloning and opaque-response traps, and the Workbox equivalents.

A caching strategy is the rule a service worker's `fetch` handler applies to decide between the Cache Storage copy and the network, and in which order. Five named strategies cover the space; every one of them is a short `respondWith()` body, and Workbox ships each as a class in `workbox-strategies`. The underlying primitives (`FetchEvent`, `caches.match()`, `cache.put()`) are Baseline widely available (Chrome 40, Firefox 44, Safari 11.1; BCD `api.FetchEvent`), so the choice is about trade-offs, not support.

## How it works

Each strategy answers the same three questions differently: where the first lookup goes, what happens when that lookup fails, and whether the cache is updated afterwards.

| Strategy | First lookup | On miss or failure | Cache updated | Cost the reader pays |
|---|---|---|---|---|
| Cache First | `caches.match()` | `fetch()`, then `cache.put()` | Only on a miss | A stale entry is served until its cache name changes or the entry is deleted |
| Network First | `fetch()` | `caches.match()` | On every success | One full network wait per request, plus the browser's offline detection time before the fallback |
| Stale-While-Revalidate | `caches.match()` | `fetch()` | On every request, in the background | Exactly one response is stale on every request after the first |
| Network Only | `fetch()` | Fails | No | No offline behaviour at all |
| Cache Only | `caches.match()` | Fails | No | Anything not precached is a hard failure |

Two mechanics apply to every strategy that writes to the cache. A `Response` body is a stream that can be read once, so the handler must `clone()` it before passing one copy to `cache.put()` and returning the other; forgetting the clone throws `TypeError: Failed to execute 'put' on 'Cache': Response body is already used` in Chromium. And `fetch()` only rejects on network failure: a `404` or `500` resolves normally, so a strategy that calls `cache.put()` without checking `response.ok` caches error pages.

Opaque responses (cross-origin requests made with `mode: 'no-cors'`) have `status` `0` and an unreadable body. They can be stored, but Chromium pads each one to about 7 MB in quota accounting to prevent size-based cross-origin inference, so a Cache First strategy over third-party fonts or images consumes quota far faster than the bytes transferred suggest.

Network First has a hidden latency problem: when the device is on a captive portal or a dead Wi-Fi link, `fetch()` does not reject quickly; it waits for the TCP timeout. Add an explicit timeout (`AbortSignal.timeout()`) and treat it as a miss, which is what Workbox's `NetworkFirst` `networkTimeoutSeconds` option does.

## Examples

The hand-written handlers below each implement one row of the table; the last example shows the same routes in Workbox.

### Stale-While-Revalidate with the clone done correctly

The cached copy is returned immediately when it exists; the network response refreshes the cache for the next request. The `clone()` happens before `put()` and the returned `Response` is the original.

```js
self.addEventListener('fetch', (event) => {
  if (event.request.destination !== 'image') return;
  event.respondWith((async () => {
    const cache = await caches.open('images-v1');
    const cached = await cache.match(event.request);
    const network = fetch(event.request).then((response) => {
      if (response.ok) cache.put(event.request, response.clone());
      return response;
    });
    event.waitUntil(network.catch(() => {}));   // keep the worker alive for the refresh
    return cached ?? network;
  })());
});
```

Without `event.waitUntil(network)` the browser may terminate the worker after `respondWith()` settles, before the background refresh completes; the `catch` keeps a network failure from surfacing as an unhandled rejection.

### Network First with a timeout and an offline fallback

The handler races the network against a three-second timer; a timeout or a failure falls back to the cache and, for navigations, to a precached `/offline.html`.

```js
self.addEventListener('fetch', (event) => {
  if (event.request.mode !== 'navigate') return;
  event.respondWith((async () => {
    const cache = await caches.open('pages-v1');
    try {
      const response = await fetch(event.request, { signal: AbortSignal.timeout(3000) });
      if (response.ok) cache.put(event.request, response.clone());
      return response;
    } catch {
      return (await cache.match(event.request)) ?? cache.match('/offline.html');
    }
  })());
});
```

`AbortSignal.timeout()` is available in Chrome 103, Firefox 100, and Safari 16 (BCD `api.AbortSignal.timeout_static`); in older workers, construct an `AbortController` and call `abort()` from `setTimeout`.

### Detecting Cache Storage before relying on a strategy

`caches` is only exposed in secure contexts (HTTPS and `localhost`; BCD `api.caches`), and a page served over plain HTTP has no `navigator.serviceWorker.register()` either. A page that depends on precached assets should check before registering the worker, and the worker should treat a missing `caches` as Network Only.

```js
// In the page
if ('serviceWorker' in navigator && 'caches' in window) {
  navigator.serviceWorker.register('/sw.js');
} else {
  // No Cache Storage: the app runs online-only; do not advertise offline support.
}

// In the worker
self.addEventListener('fetch', (event) => {
  if (!('caches' in self)) return;             // fall through to the network
  // …strategy code…
});
```

Returning from the handler without calling `respondWith()` lets the browser perform the request as if no worker existed, which is the correct degraded behaviour.

### The same routes in Workbox

Each class in `workbox-strategies` implements one row of the table above and adds the clone, the `ok` check (via `CacheableResponsePlugin`), and the timeout as options.

```js
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';

registerRoute(({ request }) => request.mode === 'navigate',
  new NetworkFirst({ cacheName: 'pages', networkTimeoutSeconds: 3 }));
registerRoute(({ request }) => request.destination === 'image',
  new StaleWhileRevalidate({ cacheName: 'images' }));
registerRoute(({ url }) => url.pathname.startsWith('/assets/'),
  new CacheFirst({ cacheName: 'assets', plugins: [new CacheableResponsePlugin({ statuses: [200] })] }));
```

Without `CacheableResponsePlugin`, `CacheFirst` and `StaleWhileRevalidate` cache any response with status `200` or `0` (opaque) by default; `NetworkFirst` caches `200` only.

:::observed
In Chrome DevTools (English UI), a request answered by any of these strategies shows `(ServiceWorker)` in the Network panel's **Size** column, and the worker's own `fetch()` appears as a second row with the gear icon that marks requests initiated from a service worker. Caching a response without cloning it fails in the worker console with `TypeError: Failed to execute 'put' on 'Cache': Response body is already used`.
:::

## See also

- [Service Workers specification: FetchEvent](https://www.w3.org/TR/service-workers/#fetchevent) (w3.org)
- [Workbox strategies module](https://developer.chrome.com/docs/workbox/modules/workbox-strategies) (developer.chrome.com)
- [Cache API](/reference/service-worker/cache-api/)
- [FetchEvent and routing](/reference/service-worker/fetch-event/)
- [Offline fallback](/reference/service-worker/offline-fallback/)
- [Workbox](/reference/service-worker/workbox/)