← All articles
WEB PERFORMANCE HTTP Caching Headers and CDN Strategy: A Practical D... 2026-03-04 · 4 min read · caching · http · cdn

HTTP Caching Headers and CDN Strategy: A Practical Developer Guide

Web Performance 2026-03-04 · 4 min read caching http cdn cache-control performance web-performance headers

Caching is one of the highest-leverage performance optimizations available — and one of the most commonly misconfigured. A correct caching strategy means returning content instantly from a cache that's close to the user, dramatically reducing latency and server load. A wrong one means stale content, cache poisoning risks, or disabling caching entirely out of caution.

Understanding HTTP caching headers gives you the control to get this right.

The Cache-Control Header

Cache-Control is the primary mechanism for telling browsers and CDNs how to cache a response. It's a directive-based header with multiple directives that can be combined.

Key Directives

max-age=<seconds>: How long the cached response is "fresh." After this time, the cache considers the response stale and must revalidate with the origin.

Cache-Control: max-age=3600

This means: cache this response, consider it fresh for 1 hour.

s-maxage=<seconds>: Same as max-age but applies only to shared caches (CDNs, proxies). Overrides max-age for CDNs while browsers use max-age.

Cache-Control: max-age=60, s-maxage=86400

This tells browsers to cache for 1 minute but CDNs to cache for 24 hours.

no-store: Never cache this response. Use for sensitive data (bank transactions, personal health info).

no-cache: This is misleading — it doesn't mean "don't cache." It means "always revalidate with the origin before serving from cache." Cache can store the response, but must check with the server each time.

public: This response can be stored by shared caches (CDNs). Defaults to inferred when max-age is set.

private: Only the browser can cache this, not CDNs. Use for personalized content that varies per user.

immutable: Tells the browser that this resource will never change. Suppresses revalidation requests during the max-age window, even when the user reloads.

Cache-Control: max-age=31536000, immutable

This is the ideal directive for versioned static assets.

Practical Recipes by Content Type

Versioned static assets (JS bundles, CSS with hash in filename, fonts):

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

Cache forever. When the file changes, you deploy a new URL.

HTML pages:

Cache-Control: public, max-age=0, must-revalidate

Or with short freshness: Cache-Control: public, max-age=60, stale-while-revalidate=300

Don't cache HTML indefinitely — users expect page updates to appear.

API responses (mostly static):

Cache-Control: public, max-age=300, stale-while-revalidate=60

API responses (personalized/user-specific):

Cache-Control: private, max-age=0, no-cache

Authenticated API responses (no caching):

Cache-Control: no-store

ETags and Conditional Requests

An ETag is a fingerprint (hash) of the response content. When a cached response is stale, the browser sends the ETag back to the server in an If-None-Match header. If the content hasn't changed, the server responds with 304 Not Modified (no body), saving bandwidth.

# Server sends:
ETag: "abc123"
Cache-Control: max-age=60

# After 60s, browser sends:
GET /api/data
If-None-Match: "abc123"

# If unchanged, server responds:
304 Not Modified

Most web frameworks and CDNs handle ETag generation automatically. For static files, ETags are typically based on file modification time and size.

Last-Modified works similarly but uses timestamps instead of hashes:

Last-Modified: Tue, 04 Mar 2026 00:00:00 GMT

Subsequent requests use If-Modified-Since. ETags are generally preferred because they handle cases where modification time changes without content changing.

The Vary Header

The Vary header tells caches that different versions of a response exist based on request headers. Without Vary, a CDN might serve a cached English response to a French user.

Vary: Accept-Language, Accept-Encoding

Vary: Accept-Encoding: Almost always correct for text content — the CDN stores separate gzip and brotli compressed versions.

Vary: Authorization: Dangerous! This caches per Authorization header value, which can be correct but is often misconfigured to cache sensitive data it shouldn't.

Vary: Cookie: Generally a red flag for CDN caching — each cookie variation creates a separate cache entry, defeating caching entirely. Instead, separate your personalized content into separate API calls and cache the static shell aggressively.

CDN-Specific Considerations

Cloudflare

Cloudflare respects Cache-Control headers but adds its own layer. A few key behaviors:

Cache busting: When you need to invalidate cached content, Cloudflare's API allows purging by URL, by cache tag (Cache-Tag: product-123), or purging everything.

curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
  -H "Authorization: Bearer {token}" \
  -d '{"files":["https://example.com/api/products"]}'

Vercel / CDN Automatic Behavior

Vercel automatically sets Cache-Control: public, max-age=0, must-revalidate on pages and Cache-Control: public, max-age=31536000, immutable on static assets based on file type. Override with custom headers in vercel.json:

{
  "headers": [
    {
      "source": "/api/(.*)",
      "headers": [{"key": "Cache-Control", "value": "s-maxage=60, stale-while-revalidate=300"}]
    }
  ]
}

Stale-While-Revalidate and Stale-If-Error

Two directives that improve perceived performance and resilience:

stale-while-revalidate=<seconds>: Serve a stale cached response immediately, then fetch a fresh version in the background. The user doesn't wait.

Cache-Control: max-age=60, stale-while-revalidate=300

After 60 seconds the response is stale, but for the next 5 minutes the cache can serve the stale version while asynchronously fetching fresh data.

stale-if-error=<seconds>: If the origin is down or returns a 5xx error, serve the stale cached response for this duration.

Cache-Control: max-age=3600, stale-if-error=86400

Extremely valuable for reliability — if your API server has a brief outage, users get cached content instead of errors.

Common Mistakes

Caching by URL with query strings: ?user=123&token=abc in a URL causes some caches to cache per unique URL. Use private or no-store for authenticated endpoints.

Not caching at the CDN but caching in the browser: Without s-maxage or explicit public, many CDNs won't cache even if max-age is set. Be explicit.

Forgetting to set Vary: Accept-Encoding: Your server might be compressing responses, but if the CDN doesn't know about it via Vary, it might serve a compressed response to a client that doesn't support it.

Treating no-cache as "don't cache": Use no-store if you truly never want caching. no-cache just requires revalidation.


A good caching strategy starts with knowing which content changes frequently versus infrequently, then matching headers to those expectations. Static assets with content-hashed URLs should be cached forever. HTML should revalidate frequently. APIs depend on their update frequency and personalization level. Get these right and you'll dramatically improve performance while reducing your origin server load.