Performance

Fix Cache-Control Headers — Why Your CDN Isn't Caching

If Cache-Control is missing, CDNs default to not caching — or make inconsistent decisions based on other headers. Explicit Cache-Control headers give you full control over what gets cached and for how long.

Check your current headers first

curl -I https://yoursite.com/
curl -I https://yoursite.com/static/app.js
curl -I https://yoursite.com/api/data

If you see no Cache-Control or Cache-Control: no-cache on static assets, your CDN is not caching them — every request goes to your origin server.

Cache-Control by resource type

Versioned static assets (JS, CSS, fonts)

These have content hashes in the filename (app.abc123.js). The content never changes, so cache them forever:

Cache-Control: public, max-age=31536000, immutable

immutable tells browsers not to revalidate during max-age, even on back/forward navigation. Removes unnecessary conditional requests.

Non-versioned static assets (images, logo.png)

Cache for a week or month, but allow revalidation:

Cache-Control: public, max-age=604800, stale-while-revalidate=86400

HTML pages

Cache briefly or not at all — HTML references your versioned assets, so you need users to get fresh HTML when you deploy:

# Option 1 — no CDN cache (always fresh)
Cache-Control: public, max-age=0, must-revalidate

# Option 2 — CDN cache with instant invalidation
Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400

API responses (non-authenticated)

# Cache for 60 seconds at CDN, serve stale for 24h while revalidating
Cache-Control: public, s-maxage=60, stale-while-revalidate=86400

API responses (authenticated)

# Never cache at CDN — must use private
Cache-Control: private, no-store

Config by stack

Nginx

location ~* \.(js|css|woff2|png|jpg|gif|ico|svg)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable" always;
}

location ~* \.html$ {
    add_header Cache-Control "public, max-age=0, must-revalidate" always;
}

Vercel (vercel.json)

{
  "headers": [
    {
      "source": "/static/(.*)",
      "headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
    },
    {
      "source": "/(.*\.html)",
      "headers": [{ "key": "Cache-Control", "value": "public, max-age=0, must-revalidate" }]
    }
  ]
}

Cloudflare

Cloudflare Dashboard → Caching → Cache Rules → Create rule → File extension matches js,css,png,jpg,woff2 → Edge Cache TTL: 1 year.

Use EdgeFix to audit your current caching headers and see exactly what the CDN is receiving for each resource type.

Run a live PageSpeed audit → SpeedFixer