# Static assets have appropriate Cache-Control headers

- **Pattern:** `ab-002070` (`performance-load.caching.static-cache-headers`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002070
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002070)

## Why it matters

Static assets served without long-lived `Cache-Control` headers force every returning visitor to re-download CSS, JavaScript, and image files they already have — identical bytes transferred repeatedly for no reason. A 500KB bundle re-downloaded on every visit costs 500KB × daily_active_users in bandwidth per day. With `Cache-Control: public, max-age=31536000, immutable`, that same bundle is downloaded once per user per year. For users on metered connections or high-latency networks, the cache miss is also the difference between a 500ms and a 3-second load. ISO 25010 performance-efficiency.resource-utilization flags repeat downloads of identical resources as a direct waste.

## Severity rationale

High because missing cache headers on static assets force full re-downloads on every page visit, wasting bandwidth and adding measurable latency for returning users.

## Remediation

Configure `Cache-Control: public, max-age=31536000, immutable` for all hashed static assets, and `max-age=0, must-revalidate` for HTML files. On Vercel and Netlify, Next.js applies these headers to hashed chunks automatically — verify no override is negating the defaults.

```json
// vercel.json — explicit headers for custom infrastructure
{
  "headers": [
    {
      "source": "/_next/static/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
      ]
    },
    {
      "source": "/(.*)\\.(js|css|woff2|png|webp|avif)$",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
      ]
    },
    {
      "source": "/(.*)\\.html$",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=0, must-revalidate" }
      ]
    }
  ]
}
```

For custom Express servers: `app.use('/static', express.static('public', { maxAge: '1y', immutable: true }))`

## Detection

- **ID:** `static-cache-headers`
- **Severity:** `high`
- **What to look for:** Count all locations where cache headers could be configured: framework config (`next.config.*` headers), deployment config (`vercel.json`, `netlify.toml`), middleware, and custom server files. For each, check the `Cache-Control` value for static assets (CSS, JS, images). Enumerate: "X cache configuration locations checked." For a deeper analysis of security headers including CSP and HSTS, the Security Headers Audit (`security-headers`) covers this in detail.
- **Pass criteria:** Static assets (JS, CSS, images) have `Cache-Control: public, max-age=31536000` or similar long expiration (at least 604800 seconds / 1 week, ideally 31536000 / 1 year). HTML has short cache (`max-age=0, must-revalidate`) or no-cache. **Pass by platform default when Vercel, Netlify, or similar managed hosting is detected** — `Cache-Control: public, max-age=31536000, immutable` is applied to all hashed static chunks automatically without explicit configuration. Note this in the detail field as a platform-dependent pass (e.g., `"Pass: Vercel + Next.js applies long-lived cache headers to hashed static assets automatically."`).
- **Fail criteria:** A non-managed hosting setup is detected (Dockerfile, custom server, bare AWS config, nginx config) and no explicit `Cache-Control` configuration is found for static assets. Or managed hosting is detected but explicit configuration actively overrides the defaults with `max-age` under 604800 (1 week).
- **Skip (N/A) when:** Hosting platform truly cannot be determined AND no server configuration files exist (0 deployment config files found).
- **Detail on fail:** `"0 of 3 cache configuration locations set long-lived headers — CSS and JS served with default short cache on custom Express server"` or `"vercel.json overrides default caching with max-age=3600 (1 hour) instead of 31536000 (1 year)"`
- **Remediation:** Configure cache headers in deployment config:

  ```json
  {
    "headers": [
      {
        "source": "/(.*)\\.(js|css|woff2|png|jpg|webp)$",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "public, max-age=31536000, immutable"
          }
        ]
      },
      {
        "source": "/(.*)\\.(html)$",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "public, max-age=0, must-revalidate"
          }
        ]
      }
    ]
  }
  ```
- **Cross-reference:** For security-related cache headers (preventing sensitive data caching), the Security Headers audit covers Cache-Control in the context of security.

## External references

- iso-25010 performance-efficiency.resource-utilization

Taxons: performance

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