# "Serve static assets with an efficient cache policy": how to fix it

**Audit ID:** `cache-insight` (formerly `uses-long-cache-ttl`) · **Category:** Performance

<!--QA-->
> **Quick answer:** This audit fails when static files are served with a short or missing Cache-Control lifetime, so repeat visitors re-download bytes they already have. Fix it by fingerprinting asset filenames, then serving them with Cache-Control: public, max-age=31536000, immutable. Keep HTML on a short lifetime so deploys still reach people.
<!--/QA-->

Lighthouse 13 renamed this audit to **"Use efficient cache lifetimes"** (`cache-insight`), but it is the same check. It fails when your static files come back with a short cache lifetime, so returning visitors download bytes they already have. In our study of 269 Shopify storefronts, 246 of them (91.4%) failed it, making it the most-failed audit in the whole dataset.

## TL;DR

- **What:** Static assets (JS, CSS, fonts, images) are served with a short or missing `Cache-Control` lifetime.
- **Why it matters:** Only repeat visits are affected, so it will not move your first-load score, but it is close to free bandwidth and latency savings for real users.
- **Fix:** Fingerprint filenames, then serve them with `Cache-Control: public, max-age=31536000, immutable`. Never long-cache your HTML.

## What is browser caching?

Browser caching is the browser storing a copy of a file on disk so the next page that needs it does not have to fetch it again. The server controls this with response headers, mainly `Cache-Control`.

There are two levels, and confusing them is the most common reason people "fix" this audit and see no change:

- **Freshness (`max-age`).** The browser can reuse the file with no network request at all. This is the one that makes repeat visits fast.
- **Revalidation (`ETag` / `Last-Modified`).** The browser still makes a request, and the server answers `304 Not Modified` with no body. You save the download, but you still pay the round trip.

Plenty of sites have ETags and think they are cached. Lighthouse is asking for the first kind.

## What does "serve static assets with an efficient cache policy" mean?

Lighthouse lists every static resource on the page alongside the cache lifetime the server declared, and estimates how much transfer a longer lifetime would save on a repeat visit. Assets with no `Cache-Control` at all, or with a lifetime of minutes or hours, are what show up.

Two things worth knowing about how it scores:

- **It only ever describes repeat visits.** A first-time visitor is unaffected, which is why fixing this rarely changes your headline performance number even though it is a real improvement.
- **It counts third-party assets too**, which you usually cannot control (see below).

## How do I leverage browser caching?

The phrase comes from older tools like GTmetrix, but it means the same thing. Three steps, in this order, because doing them out of order is what makes caching scary:

1. **Fingerprint the filenames first.** Give each build's assets a content hash, for example `app.4f9a2c1.js`. Every modern bundler does this by default. This is the step that makes long caching safe: when the content changes, the URL changes, so there is nothing stale to serve.
2. **Set a long lifetime on those fingerprinted files.** One year, plus `immutable`.
3. **Leave HTML on a short lifetime.** The HTML is what points at the new hashed filenames, so it has to stay fresh or nobody ever sees your deploys.

## What Cache-Control values should I use?

```
# Fingerprinted static assets: JS, CSS, fonts, hashed images
Cache-Control: public, max-age=31536000, immutable

# HTML: always revalidate, never serve stale
Cache-Control: no-cache

# Unhashed but rarely changing files, for example /favicon.ico
Cache-Control: public, max-age=86400
```

What the pieces do:

- **`max-age=31536000`** is one year in seconds, the practical maximum.
- **`immutable`** tells the browser not to revalidate even on a hard reload. Without it, some browsers still send conditional requests for cached files.
- **`no-cache`** does NOT mean "do not cache". It means "cache it, but check with the server before reusing it". The header that actually forbids storage is `no-store`, which you want for authenticated or personalized responses, not for your homepage.
- **`s-maxage`** overrides `max-age` for shared caches like a CDN, so you can keep a long CDN lifetime and a short browser one.
- **`stale-while-revalidate=60`** lets the browser use a slightly stale copy while it fetches a fresh one in the background. Good for assets that change occasionally and where instant is worth more than exact.

## How do I cache-bust a file that changes?

If your filenames are hashed, you do not have to. That is the whole point: `app.4f9a2c1.js` and `app.8b1e7d3.js` are different URLs, so the new deploy simply is not in the cache.

If you cannot hash filenames, a query string (`/style.css?v=3`) works in every browser that matters, though some intermediary proxies historically ignored it. What does not work is bumping the version on some references and not others, which leaves visitors with a mixed set of old and new files.

The failure mode to avoid: setting `max-age=31536000` on unhashed filenames. Ship a bug and some users keep the broken file for a year, with no way for you to reach them.

## How do I set cache headers on my platform?

**Nginx**

```nginx
location ~* \.(js|css|woff2|png|jpg|svg)$ {
  add_header Cache-Control "public, max-age=31536000, immutable";
}
```

**Apache**

```apache
<FilesMatch "\.(js|css|woff2|png|jpg|svg)$">
  Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
```

**Vercel / Netlify.** Both serve hashed build output with a one-year lifetime automatically. What usually fails the audit is files you put in the public or static directory yourself, since those are served with a short default. Set headers explicitly in `vercel.json` or `_headers`.

**Cloudflare and other CDNs.** The CDN can add cache headers, but the browser only sees what the CDN returns. Check the response you actually get, not the origin config.

**WordPress.** Caching plugins mostly handle page caching (the HTML), which is a different problem. Static asset headers come from the web server or your CDN, so fix it there.

**Shopify.** Theme assets from the Shopify CDN already carry long lifetimes. The failures in our study came almost entirely from third-party app scripts, which is exactly the case you cannot fix directly.

## Why does Lighthouse still flag third-party scripts?

Because vendors choose their own lifetimes, and many pick short ones deliberately so they can push changes quickly. Google Analytics is the classic example: a short TTL on a file loaded by a large share of the web.

You cannot set headers on a domain you do not control, so your options are the same three as always: drop the tag if it is not earning its place, self-host it if the licence allows and you accept owning the updates, or accept the finding. Self-hosting a vendor script means you no longer get their fixes automatically, so it is a real trade, not a free win.

This is worth saying plainly: a page can be correctly configured and still show entries in this audit. Chasing it to zero is not the goal.

## How do I verify the fix?

1. Re-run Lighthouse and check that your own assets have left the list.
2. Confirm the header on a real response, not in your config:

   ```bash
   curl -sSI https://example.com/assets/app.4f9a2c1.js | grep -i cache-control
   ```

3. Load the page twice in DevTools with the Network tab open. On the second load, cached files should read `(disk cache)` or `(memory cache)` in the Size column, with no request made.
4. Deploy a change and confirm visitors get it. If the HTML is on `no-cache` and assets are hashed, they will.

## What mistakes should I avoid?

- **Long-caching HTML.** Your deploys stop reaching people, and it is invisible until someone complains they are seeing an old page.
- **Assuming `no-cache` prevents caching.** It permits caching with revalidation. `no-store` is the one that forbids it.
- **Long `max-age` on unhashed filenames.** A shipped bug becomes unreachable for a year.
- **Fixing this expecting a score jump.** It affects repeat visits only. It is worth doing for users, not for the number.
- **Reading the origin config instead of the response.** A CDN or proxy in front of you can rewrite or strip the header.

## Related audits

- [Enable text compression](/audits/enable-text-compression), the other header-level win on the same static assets
- [Reduce unused JavaScript](/audits/reduce-unused-javascript), caching a bundle nobody needs is still shipping it
- [Browser errors were logged to the console](/audits/errors-in-console), where third-party tags show up as a different symptom of the same problem

---

Audit your URL at https://lighthouse-md.com.
