Cache-Control Headers: The Definitive Deep Dive

· James Thornton · 9 min read

Cache-Control is the single most important HTTP header for web performance. It governs how browsers, CDN edge servers, and intermediate proxies store, reuse, and revalidate cached responses. A misconfigured Cache-Control header can either leave your origin servers overwhelmed with unnecessary traffic or serve stale content to users long after it has changed. Understanding every directive in the Cache-Control family is essential for any engineer working with CDN caching strategies or optimizing delivery pipelines.

What Is the Cache-Control Header?

Cache-Control is an HTTP response header defined in RFC 9111 that specifies caching directives for both requests and responses. Unlike its predecessor Pragma, Cache-Control provides granular control over caching behavior across every layer of the delivery chain. The header accepts a comma-separated list of directives, each instructing caches on storage permissions, freshness durations, and revalidation requirements.

CDN Cache Hierarchy — Multi-tier caching architecture from edge to originCDN Cache Hierarchy — Multi-tier caching architecture from edge to origin CDN Cache Hierarchy — Multi-Tier Architecture L1 Edge Cache Hit Rate: 95% Latency: < 5ms miss L2 Regional Cache Hit Rate: 85% Latency: 10–30ms miss Origin Shield Hit Rate: 70% Latency: 50–80ms miss Origin Server Hit Rate: — Latency: 100–300ms
Figure 1: CDN Cache Hierarchy — Multi-tier caching architecture from edge to origin

A typical Cache-Control header looks like this:

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

This tells every cache in the chain to store the response, consider it fresh for one year, and skip revalidation even on page reload. Each directive serves a specific purpose, and combining them correctly is the key to optimal caching behavior.

How Do Freshness Directives Work?

max-age

The max-age directive sets the maximum time in seconds that a response is considered fresh. After this period expires, the cache must revalidate with the origin before serving the response again. This directive applies to all caches including browsers and CDN edges.

Performance Comparison: Edge vs Origin Processing
Metric Edge Processing Origin Processing Improvement
Latency (p50)8–15 ms120–300 ms15–20x faster
Cache Hit Ratio85–95%N/AReduces origin load
TLS Handshake1–5 ms (nearby PoP)50–150 ms10–30x faster
Bandwidth Cost$0.01–0.04/GB$0.05–0.12/GB3–5x cheaper
Availability99.99% (multi-PoP)99.9% (single DC)10x fewer outages
Cache-Control: max-age=3600
# Response is fresh for 1 hour from the time it was generated

s-maxage

The s-maxage directive overrides max-age specifically for shared caches such as CDN edge servers and reverse proxies. Browsers ignore it. This separation is powerful because it lets you cache content at the edge for hours while forcing browsers to revalidate on every visit.

Cache-Control: max-age=0, s-maxage=86400
# Browsers always revalidate; CDN caches for 24 hours

This pattern is common for HTML pages where you want the CDN to absorb traffic but need browsers to check for updates. Learn how this interacts with TTL optimization strategies for fine-grained control.

What Is the Difference Between no-cache and no-store?

The most commonly confused Cache-Control directives are no-cache and no-store. Despite their similar names, they behave very differently.

no-cache

no-cache does not mean "do not cache." It means "cache the response, but revalidate with the origin before every use." The cache stores the response and sends a conditional request (using ETag or Last-Modified) before serving it. If the origin responds with 304 Not Modified, the cache serves its stored copy without downloading the full response body again.

Cache-Control: no-cache
# Cache stores it, but always checks with origin first

no-store

no-store instructs caches to never store any part of the response. Every request results in a full fetch from the origin. Use this for genuinely sensitive data such as financial transactions, medical records, or authentication tokens.

Cache-Control: no-store
# Nothing is cached anywhere — full origin fetch every time
DirectiveStored in cache?RevalidationUse case
no-cacheYesAlways before useFrequently changing content
no-storeNoN/A (never cached)Sensitive or secret data
max-age=0YesImmediately staleSimilar to no-cache in practice

How Do Revalidation Directives Control Stale Content?

must-revalidate

The must-revalidate directive tells caches that once a response becomes stale (exceeds max-age), they must not serve it without first revalidating with the origin. Without this directive, caches may serve stale content when the origin is unreachable, which some HTTP implementations allow as a fallback.

Cache Decision Flow — How the CDN determines cache hit vs missCache Decision Flow — How the CDN determines cache hit vs miss CDN Request Flow — From Browser to Edge CDN Request Lifecycle Step 1 DNS Lookup Step 2 TCP Connect Step 3 TLS Handshake Step 4 Edge Server Step 5 Cache Check Step 6 Response Cache HIT → 5ms response Cache MISS → Fetch origin 0ms ~50ms (cache hit) ~200ms (cache miss)
Figure 2: Cache Decision Flow — How the CDN determines cache hit vs miss

proxy-revalidate

Functions identically to must-revalidate but applies only to shared caches (CDN edges, reverse proxies). Browsers ignore it. This lets you enforce strict revalidation at the CDN layer while allowing browsers to show stale content as a fallback during network issues.

immutable

The immutable directive tells browsers that the response body will not change during its freshness lifetime. Without it, browsers send conditional revalidation requests when users hit the reload button, even if the content is still fresh. With immutable, the browser skips these unnecessary 304 round-trips.

Cache-Control: public, max-age=31536000, immutable
# Perfect for content-hashed assets: app.3f8a2b.js

This directive is particularly valuable for static assets served through a CDN cache hierarchy where every unnecessary revalidation request adds latency.

What Are the Scope Directives: public vs private?

public

The public directive explicitly allows any cache to store the response, including shared caches like CDN edges. This is necessary when the response would otherwise be considered non-cacheable, such as responses to authenticated requests or responses with certain status codes.

private

The private directive restricts caching to the end user's browser only. Shared caches like CDN edge servers and corporate proxies must not store the response. Use this for user-specific content such as personalized dashboards, account pages, or shopping carts.

# User-specific content: browser can cache, CDN cannot
Cache-Control: private, max-age=600

# Public content: any cache can store it
Cache-Control: public, max-age=86400

Understanding the distinction between browser cache and CDN cache is essential for applying these directives correctly.

How Do Stale Content Directives Improve Resilience?

stale-while-revalidate

Defined in RFC 5861, stale-while-revalidate specifies a window of time after max-age expires during which the cache can serve the stale response immediately while fetching a fresh copy in the background. This eliminates the revalidation latency penalty for end users.

Cache-Control: max-age=300, stale-while-revalidate=60
# Fresh for 5 minutes, then serve stale for up to 1 minute
# while revalidating in the background

For a deep exploration of this pattern, see stale-while-revalidate in CDN environments.

stale-if-error

The stale-if-error directive allows caches to serve stale content when the origin returns a 5xx error or is unreachable. This provides resilience during origin outages without requiring manual intervention.

Cache-Control: max-age=600, stale-if-error=86400
# Fresh for 10 minutes; serve stale for up to 24 hours
# if the origin is down

Combining both stale directives with appropriate TTLs creates a resilient caching strategy that handles both normal operation and failure scenarios gracefully.

How Do Validation Mechanisms Work: ETag vs Last-Modified?

When a cache needs to revalidate a stored response, it uses one of two validation mechanisms to avoid downloading unchanged content.

ETag (Entity Tag)

An ETag is an opaque identifier assigned by the origin server to a specific version of a resource. The cache sends the stored ETag in an If-None-Match header. If the current version matches, the origin responds with 304 Not Modified.

# Origin response:
ETag: "a1b2c3d4e5"
Cache-Control: no-cache

# Cache revalidation request:
If-None-Match: "a1b2c3d4e5"

# Origin response (unchanged):
304 Not Modified

Last-Modified

The Last-Modified header contains the date the resource was last changed. The cache sends this timestamp in an If-Modified-Since header. ETags are preferred because they handle sub-second changes and content-negotiated responses more accurately.

MechanismRequest headerPrecisionBest for
ETagIf-None-MatchByte-level accuracyDynamic content, API responses
Last-ModifiedIf-Modified-Since1-second granularityStatic files with known modification dates

Both mechanisms work across CDN layers. When a CDN edge revalidates with the origin, the conditional request avoids transferring the full response body, saving bandwidth and reducing request flow latency.

What Does the Vary Header Do for Caching?

The Vary header tells caches which request headers to use as part of the cache key. Each unique combination of Vary header values creates a separate cached entry.

Vary: Accept-Encoding
# Separate cache entries for gzip, brotli, and identity responses

Common and safe Vary values include Accept-Encoding (handled natively by most CDNs) and Accept-Language (for multilingual sites). Dangerous Vary values include Cookie, Authorization, and User-Agent, which create so many cache variants that hit rates collapse.

Most CDNs normalize Vary: Accept-Encoding automatically, storing only two or three compressed variants. For device-based variations, use CDN-specific device detection headers rather than Vary: User-Agent, which creates thousands of unique entries and effectively disables caching.

Is the Pragma Header Still Relevant?

Pragma: no-cache is a holdover from HTTP/1.0 that functions similarly to Cache-Control: no-cache. Every modern browser and CDN supports Cache-Control, making Pragma unnecessary. However, some organizations include it for defense-in-depth against extremely old proxy servers.

# Legacy compatibility pattern (rarely needed)
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0

If you are setting Cache-Control headers correctly, adding Pragma provides no additional benefit. For an overview of caching fundamentals including header evolution, see HTTP caching fundamentals.

What Are the Recommended Headers for Each Content Type?

Content typeRecommended Cache-ControlRationale
Content-hashed JS/CSSpublic, max-age=31536000, immutableFilename changes on update; cache forever
Non-hashed JS/CSSpublic, max-age=86400, must-revalidateDaily expiry; force revalidation when stale
Images and fontspublic, max-age=259200030-day freshness; stable content
HTML pagespublic, max-age=0, s-maxage=300, stale-while-revalidate=60CDN caches 5 min; browser always checks
Authenticated APIprivate, no-cacheUser-specific; always revalidate
Public APIpublic, max-age=60, stale-while-revalidate=30Short freshness with background refresh
Sensitive datano-storeNever cached anywhere

These recommendations serve as starting points. Your optimal values depend on content update frequency, traffic patterns, and origin capacity. Use cache hit ratio monitoring to measure whether your header strategy delivers the expected results, and adjust TTL values based on real traffic data.

Common Cache-Control Mistakes to Avoid

Even experienced engineers make header configuration errors that silently degrade performance or cause stale content issues:

Cache-Control headers are the foundation of every caching layer in your delivery pipeline. Mastering their directives, understanding the interaction between browser and CDN caches, and selecting appropriate validation mechanisms puts you in control of both performance and freshness. Test your headers with browser DevTools, verify CDN behavior with response headers like X-Cache and Age, and monitor your CDN performance metrics to confirm that your configuration delivers the expected results.

External References

References & Standards: HTTP/2 specification · Cloudflare documentation