# Cache-Control headers optimized for hashed assets

- **Pattern:** `ab-002027` (`performance-deep-dive.caching-cdn.cache-control-hashed-assets`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002027
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002027)

## Why it matters

Hashed assets are designed for aggressive caching — the filename changes only when the file content changes, so browsers can safely cache them forever. Without `immutable, max-age=31536000`, browsers re-validate these files on every visit, adding a round-trip for each asset even when the content is identical. Conversely, if HTML files are cached with a long `max-age`, users miss updates entirely until the cache expires. ISO 25010:2011 time-behaviour frames the latency; the business impact is repeat visitors waiting on unnecessary network round-trips.

## Severity rationale

Critical because misconfigured cache headers force re-validation of every asset on every visit, eliminating the performance gains that content-hashed filenames are specifically designed to provide.

## Remediation

Set `Cache-Control: public, immutable, max-age=31536000` for hashed assets and `no-cache` for HTML in your deployment config.

```json
// vercel.json
{
  "headers": [
    {
      "source": "/_next/static/(.*)",
      "headers": [{ "key": "Cache-Control", "value": "public, immutable, max-age=31536000" }]
    },
    {
      "source": "/(.*)\.html",
      "headers": [{ "key": "Cache-Control", "value": "public, no-cache" }]
    }
  ]
}
```

## Detection

- **ID:** `cache-control-hashed-assets`
- **Severity:** `critical`
- **What to look for:** Count all cache-related HTTP headers in deployment config. Enumerate the `Cache-Control` values for hashed assets vs. HTML files. Examine the deployment config (vercel.json, netlify.toml, _headers, or equivalent) and HTTP headers sent by the application. For hashed/immutable assets (files with content hash in name like `app.a1b2c3d4.js`), check for `Cache-Control: immutable, max-age=31536000` (1 year). For HTML files, check for `Cache-Control: no-cache` or `must-revalidate`.
- **Pass criteria:** Hashed assets (JavaScript, CSS, images with content hash filenames) are cached with `immutable` and `max-age=31536000` (1 year). HTML files use `no-cache` or `must-revalidate` to ensure latest version is checked before serving. Hashed assets must have `max-age` of at least 31536000 (1 year).
- **Fail criteria:** Hashed assets use short cache times (max-age < 31536000) or no cache headers. HTML files are cached with `max-age > 0` without `no-cache`.
- **Skip (N/A) when:** Never — HTTP caching is fundamental for performance.
- **Do NOT pass when:** Cache-Control headers exist but are set on the wrong path patterns, caching HTML with immutable or caching unhashed assets with long max-age.

- **Cross-reference:** For asset hashing strategy, see the `asset-content-hash` check. For CDN cache hit ratio, see `cdn-cache-hit-ratio`.
- **Detail on fail:** `"Hashed assets cached with max-age=86400 (1 day) instead of 1 year"` or `"HTML files cached with Cache-Control: max-age=3600, preventing users from getting updates"`
- **Remediation:** Configure HTTP caching headers on your deployment platform properly.

  ```json
  // vercel.json — cache headers
  { "headers": [{ "source": "/_next/static/(.*)", "headers": [{ "key": "Cache-Control", "value": "public, immutable, max-age=31536000" }] }] }
  ```

## External references

- iso-25010 time-behaviour

Taxons: performance

HTML version: https://auditbuffet.com/patterns/ab-002027
