# Choosing a caching strategy

> Pick a service worker caching strategy per resource type: cache-first, network-first, stale-while-revalidate, network-only, or cache-only, and what each costs.

At the end of this guide each kind of request your service worker intercepts has a named
strategy, you have the five strategies as plain functions you can paste into `sw.js`, and
you know what each one costs in freshness, speed, or offline availability. The right choice
depends on whether a resource is static, dynamic, or time-sensitive, and on how stale a
response your users can tolerate.

You need a service worker with a `fetch` handler (see
[Offline strategies](/guides/offline/) for the handler that routes requests to these
functions). The `Cache` interface the functions use is separate from the HTTP cache:
`Cache-Control` headers decide what the browser's HTTP cache keeps, and have no influence
on what `cache.put()` stores or how long `caches.match()` returns it.

## The core strategies

Each function takes a `Request` and returns a `Response`, so a `fetch` handler can call
`event.respondWith(strategy(event.request))`. `cache.put()` consumes the response body,
which is why every function clones before storing.

### Cache-first

Check the cache; go to the network only on a miss, and store what comes back.

```js
async function cacheFirst(request, cacheName = 'static') {
  const cached = await caches.match(request);
  if (cached) return cached;
  const response = await fetch(request);
  if (response.ok) {
    const cache = await caches.open(cacheName);
    cache.put(request, response.clone());
  }
  return response;
}
```

Use it for hash-versioned assets (JavaScript bundles, CSS, images, fonts) that do not
change under the same URL. The response is instant and works offline; the cost is that a
cached entry is not refreshed until the service worker version changes or the entry is
deleted.

### Network-first

Try the network; fall back to the cached copy when the request fails.

```js
async function networkFirst(request, cacheName = 'dynamic', timeoutMs = 3000) {
  const cache = await caches.open(cacheName);
  try {
    const response = await Promise.race([
      fetch(request),
      new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeoutMs)),
    ]);
    if (response.ok) cache.put(request, response.clone());
    return response;
  } catch {
    const cached = await cache.match(request);
    if (cached) return cached;
    throw new Error(`no network and no cached copy for ${request.url}`);
  }
}
```

Use it for HTML and API responses where the newest version matters online and the last
seen version is still useful offline. The cost is a full network wait on every request,
and on a flaky connection the user waits for the failure before the cache answers; the
timeout above caps that wait.

### Stale-while-revalidate

Return the cached response at once and refresh the cache in the background for next time.

```js
async function staleWhileRevalidate(request, cacheName = 'dynamic') {
  const cache = await caches.open(cacheName);
  const cached = await cache.match(request);
  const refresh = fetch(request).then((response) => {
    if (response.ok) cache.put(request, response.clone());
    return response;
  });
  return cached || refresh;
}
```

Use it for content that should be fast and reasonably current but can be one version
behind: avatars, article lists, UI chrome. The cost is one extra request per load and a
response that is stale by exactly one visit.

### Network-only

Do not consult the cache at all. In a `fetch` handler this means not calling `respondWith()` at
all, so the browser handles the request as if no worker existed.

```js
self.addEventListener('fetch', (event) => {
  if (event.request.method !== 'GET') return; // network-only: POST, PUT, DELETE
});
```

Use it for non-GET requests, real-time data, and anything that must not go stale. It
does not work offline, by definition.

### Cache-only

Serve exclusively from the cache and fail on a miss.

```js
async function cacheOnly(request) {
  const cached = await caches.match(request);
  if (cached) return cached;
  return new Response('Not precached', { status: 504 });
}
```

Use it for resources you precached in `install`: the app shell and the offline fallback
page. The cost is that nothing updates until the next worker version precaches a new set.

## Match a strategy to each resource type

| Resource type | Strategy | Why |
|---|---|---|
| App shell (HTML, CSS, JavaScript) with hashed names | Cache-first | Immutable under one URL; instant after first visit |
| Images and fonts | Cache-first | Rarely change; large enough that refetching hurts |
| Navigation requests | Network-first with an offline fallback page | Fresh HTML online; cached shell or fallback offline |
| API data specific to the user | Network-first | Must be fresh; cache is the offline backstop |
| Shared or semi-static API data | Stale-while-revalidate | Paint from cache, refresh for next time |
| POST, PUT, DELETE, WebSocket upgrades | Network-only | Not servable from a cache |
| Precached offline page | Cache-only | Exists so the fallback does not depend on the network |

Workbox ships the same five as classes in `workbox-strategies`: `CacheFirst`,
`NetworkFirst`, `StaleWhileRevalidate`, `NetworkOnly`, and `CacheOnly`, registered per
route with `registerRoute()`. `NetworkFirst` and `NetworkOnly` take a
`networkTimeoutSeconds` option that plays the role of the timeout above; the Workbox
`StaleWhileRevalidate` issues the revalidation request on every hit regardless of the cached
entry's age ([workbox-strategies](https://developer.chrome.com/docs/workbox/modules/workbox-strategies)). The library costs a build step and a few kilobytes against hand-written
functions you own.

## Expire entries by age

Cache Storage has no expiry of its own, so a response stored a month ago is returned by
`caches.match()` exactly as stored. For resources that are neither immutable nor critical,
check the response's `Date` header and treat an old entry as a miss.

```js
async function cacheWithExpiry(request, maxAgeSeconds, cacheName = 'dynamic') {
  const cache = await caches.open(cacheName);
  const cached = await cache.match(request);
  if (cached) {
    const storedAt = new Date(cached.headers.get('date') || 0).getTime();
    if (Date.now() - storedAt < maxAgeSeconds * 1000) return cached;
    await cache.delete(request);
  }
  const response = await fetch(request);
  if (response.ok) cache.put(request, response.clone());
  return response;
}
```

The `Date` header is set by the server, so a clock that is wrong on either side shifts the
expiry; storing your own timestamp in IndexedDB alongside the cache key is more work and
more exact. Workbox's `ExpirationPlugin` does this with `maxAgeSeconds` and `maxEntries`.

:::observed
In Chrome DevTools, **Application** > **Storage** > **Cache Storage** lists each named cache;
selecting one shows its entries in a table, and selecting an entry shows the stored HTTP
headers beneath the table and the body under a **Preview** tab. An entry stored with
`Cache-Control: max-age=60` is still listed, and still returned by `caches.match()`, long
after sixty seconds have passed, because the header governs the HTTP cache and not this
store. **Delete Selected** removes one entry; **Clear site data** on the Storage view
removes every cache for the origin.
:::

## See also

- [Offline strategies](/guides/offline/)
- [Caching strategies for service workers](/reference/service-worker/caching-strategies/)
- [Cache Storage: the request/response store behind offline PWAs](/reference/storage/cache-storage/)
- [Workbox](/reference/service-worker/workbox/)
- [Strategies for service worker caching](https://developer.chrome.com/docs/workbox/caching-strategies-overview) (developer.chrome.com)
- [Service worker caching and HTTP caching](https://web.dev/articles/service-worker-caching-and-http-caching) (web.dev)
- [workbox-strategies](https://developer.chrome.com/docs/workbox/modules/workbox-strategies) (developer.chrome.com)

← Back to the [Guides](/guides/) overview.