Loading content...
Every HTTP response that traverses the internet can carry instructions about how it should be cached. These instructions are encoded in the Cache-Control header—a standardized vocabulary that browsers, CDNs, reverse proxies, and forward proxies all understand.
Cache-Control isn't just one setting; it's a collection of directives that together describe precise caching behavior. A misconfigured directive can make your CDN useless. The wrong combination can expose private data in public caches. But a well-crafted Cache-Control header is the foundation of a high-performance, secure caching architecture.
This page provides an exhaustive reference to Cache-Control, from fundamental directives you'll use daily to advanced features that enable sophisticated caching strategies.
By the end of this page, you will understand every Cache-Control directive in the HTTP specification, know which combinations work together and which conflict, master the difference between cacheability and storage directives, and be able to construct Cache-Control headers for any use case with confidence.
The Cache-Control header is part of HTTP/1.1 (RFC 7234, now RFC 9111) and supersedes the older Pragma and Expires headers. It can appear in both requests and responses, though response directives are more commonly used for CDN caching.
Header Syntax:
Cache-Control: directive1, directive2=value, directive3
Multiple directives are comma-separated. Some directives take values (e.g., max-age=3600), while others are flags (e.g., no-cache). The header is case-insensitive, but lowercase is conventional.
Directive Categories:
Cache-Control directives fall into several categories:
| Directive | Category | Appears In | Purpose |
|---|---|---|---|
| public | Audience | Response | Cacheable by any cache (CDN, browser, proxies) |
| private | Audience | Response | Cacheable only by browser (not CDN/shared caches) |
| no-store | Cacheability | Request/Response | Must not store in any cache |
| no-cache | Revalidation | Request/Response | Must revalidate before each use |
| max-age=N | Expiration | Request/Response | Fresh for N seconds from response |
| s-maxage=N | Expiration | Response | Shared cache TTL (overrides max-age for CDNs) |
| must-revalidate | Revalidation | Response | Must not serve stale, even if willing |
| proxy-revalidate | Revalidation | Response | shared caches must revalidate when stale |
| no-transform | Storage | Response | Proxies must not modify response |
| immutable | Revalidation | Response | Response will never change |
| stale-while-revalidate=N | Staleness | Response | Serve stale while revalidating for N seconds |
| stale-if-error=N | Staleness | Response | Serve stale if origin errors for N seconds |
Most Cache-Control usage is in responses (server → client). However, clients can also send Cache-Control in requests to express preferences like max-age=0 (want fresh) or no-cache (bypass cache). CDNs may or may not respect request directives depending on configuration.
Before any caching decision, the fundamental question is: Is this response cacheable at all? Cacheability directives answer this question.
public does NOT mean 'cache this'. It only permits caching. You still need max-age or similar to specify duration.Cache-Control: public, max-age=86400 — Cache anywhere for 24 hours.Cache-Control: private, max-age=300 — Browser can cache for 5 minutes, CDN must not cache.Cache-Control: no-store — No caching, no storage, no history.12345678910111213141516171819202122232425
# PUBLIC STATIC ASSET (JavaScript bundle)HTTP/1.1 200 OKCache-Control: public, max-age=31536000, immutableContent-Type: application/javascript# Anyone can cache this for 1 year # USER-SPECIFIC DASHBOARDHTTP/1.1 200 OKCache-Control: private, max-age=60Content-Type: text/html# Only browser caches; CDN must not store # AUTHENTICATION TOKEN RESPONSE HTTP/1.1 200 OKCache-Control: no-storeContent-Type: application/json# Never cache anywhere—contains secrets # COMMON MISTAKE: Conflicting directivesHTTP/1.1 200 OKCache-Control: public, no-store # CONFLICT! no-store winsCache-Control: private, public # CONFLICT! private wins # CORRECT PRIORITY (per spec):# no-store > no-cache > private > publicA common and dangerous confusion: no-cache does NOT mean 'don't cache'. It means 'cache, but always revalidate before using'. Only no-store prevents caching. Mixing these up can cause sensitive data to be cached when you thought it was excluded.
Once cacheability is established, expiration directives control how long the cached response remains fresh before requiring revalidation or replacement.
Expires header. If both present, max-age takes priority.max-age=3600 — Cache for 1 hour after response generation.max-age.s-maxage and use max-age.max-age; CDNs use s-maxage.max-age=60, s-maxage=86400 — Browser: 1 min, CDN: 24 hours.123456789101112131415161718192021222324
# PATTERN 1: Simple unified TTLCache-Control: public, max-age=3600# All caches: 1 hour # PATTERN 2: Split browser/CDN TTL (recommended for dynamic content)Cache-Control: public, max-age=60, s-maxage=86400# Browser: 1 minute (users see updates quickly)# CDN: 24 hours (reduced origin load, use purge for updates) # PATTERN 3: Immutable static assetsCache-Control: public, max-age=31536000, immutable# All caches: 1 year, no revalidation needed# Use for: /static/app.abc123.js (content-hashed filenames) # PATTERN 4: Frequently updated contentCache-Control: public, max-age=0, s-maxage=300# Browser: Always revalidate (conditional request)# CDN: 5 minutes# Use for: News feeds, stock prices, live scores # PATTERN 5: max-age=0 with must-revalidateCache-Control: public, max-age=0, must-revalidate# Equivalent to no-cache for strict caches# Ensures validation on every requestHTTP spec recommends caches limit TTL to 1 year (31536000 seconds) maximum. While not strictly enforced, setting TTLs beyond this is nonstandard and may not be honored. For truly permanent content, use content-addressed URLs (hashed filenames) combined with 1-year TTL.
Revalidation directives control how caches verify that their stored copies are still current. Understanding these is essential for scenarios where serving stale content is unacceptable.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
SCENARIO: Object cached, TTL expired, origin down ─────────────────────────────────────────────────────WITH must-revalidate:───────────────────────────────────────────────────── Client Request → CDN │ ├─ Cached object is STALE (TTL expired) ├─ must-revalidate requires origin check │ └─ Origin Request → [TIMEOUT/ERROR] │ └─ CDN Response: 504 Gateway Timeout (Must not serve stale due to must-revalidate) ─────────────────────────────────────────────────────WITHOUT must-revalidate (default behavior):───────────────────────────────────────────────────── Client Request → CDN │ ├─ Cached object is STALE (TTL expired) ├─ No must-revalidate, can consider serving stale │ └─ Origin Request → [TIMEOUT/ERROR] │ └─ CDN Response: 200 OK (stale content) Warning: 110 Response is stale (Stale is better than nothing) ─────────────────────────────────────────────────────WITH no-cache:───────────────────────────────────────────────────── Client Request → CDN │ ├─ Cached object exists (any age) ├─ no-cache requires validation on EVERY request │ └─ Origin Request (conditional) If-None-Match: "abc123" │ └─ Origin: 304 Not Modified │ └─ CDN: Serve cached version (validated fresh)must-revalidate, but only for shared caches (CDNs, proxies).Cache-Control: private, max-age=3600, proxy-revalidateCache-Control: public, max-age=31536000, immutableOnly use immutable for URLs that include a content hash (e.g., app.a1b2c3.js). If you use immutable on /app.js and push an update, users won't see it until their cache expires. Content addressing ensures URL changes when content changes, safely bypassing revalidation.
Modern HTTP caching includes sophisticated directives for handling stale content—situations where the cached object has expired but can still provide value. These directives enable graceful degradation and improved user experience during origin issues or revalidation latency.
max-age=60, stale-while-revalidate=3600 — Fresh for 1 min, then stale+revalidate for 1 hour.1234567891011121314151617181920212223
TIMELINE: Cache-Control: max-age=60, stale-while-revalidate=3600 T=0s Response cached, freshT=30s Request → Cache hit (fresh), instant 200T=60s TTL expires, object now STALET=65s Request → Cache hit (stale, within SWR window) │ ├─→ Client receives IMMEDIATE 200 (stale content) │ └─→ Background: Cache sends revalidation to origin Origin returns fresh content Cache updates stored object T=70s Request → Cache hit (FRESH from background revalidate) T=3660s SWR window expires (60 + 3600)T=3700s Request → Cache miss, synchronous origin fetch required BENEFITS:✓ Users never wait for origin during SWR window✓ Content freshness maintained (just 1 request behind)✓ Origin protection during traffic spikes✓ Eliminates "thundering herd" on popular content expirymax-age=60, stale-if-error=86400 — Serve stale for 24h during origin outages.123456789101112131415161718192021
# RECOMMENDED: Full staleness strategy for important contentHTTP/1.1 200 OKCache-Control: public, max-age=300, stale-while-revalidate=3600, stale-if-error=86400 # Breakdown:# - max-age=300: Fresh for 5 minutes# - stale-while-revalidate=3600: After 5 min, serve stale while refreshing for 1 hour# - stale-if-error=86400: If origin fails, serve stale for up to 24 hours # RESULT:# T+0 to T+5min: Fresh content served# T+5min to T+65min: Stale content served instantly, background refresh# T+65min+: Must revalidate synchronously (unless origin error)# Origin down: Serve stale for up to 24 hours (graceful degradation) # ALTERNATIVE: Aggressive availability preferenceHTTP/1.1 200 OK Cache-Control: public, max-age=60, stale-while-revalidate=86400, stale-if-error=604800 # Fresh for 1 min, then stale+refresh for 24h, stale during errors for 1 week# For content where availability >>> freshnessFor any content where 100% freshness isn't critical, stale-while-revalidate is the single most impactful caching optimization. It eliminates user-facing latency spikes when TTLs expire while maintaining near-fresh content. Use it liberally for product pages, blog posts, search results, and API responses.
Beyond the core caching directives, several additional directives handle specialized scenarios.
no-transform prevents this.Cache-Control: public, max-age=3600, no-transformCache-Control: only-if-cached in request headers.123456789101112131415161718192021222324252627
# PREVENT CARRIER IMAGE COMPRESSION# Mobile carriers often recompress images to save bandwidth# Use no-transform to preserve original qualityHTTP/1.1 200 OKContent-Type: image/pngCache-Control: public, max-age=86400, no-transform # CLIENT OFFLINE/CACHE-ONLY REQUESTGET /api/data HTTP/1.1Cache-Control: only-if-cached# If not in cache, server returns 504 instead of fetching from origin # EXTENSION DIRECTIVES (CDN-specific)# Some CDNs support extension directives # Cloudflare: CDN-Cache-Control (edge-only caching instructions)HTTP/1.1 200 OKCache-Control: max-age=60 # Browser TTLCDN-Cache-Control: max-age=86400 # Edge TTL (Cloudflare ignores for browser) # Fastly: Surrogate-Control (precedes CDN-Cache-Control standardization)HTTP/1.1 200 OKCache-Control: max-age=60Surrogate-Control: max-age=86400 # AWS: Origin response + Cache Policy interaction# CloudFront uses Cache Policy settings that can override origin headersMany CDN providers support extension headers like CDN-Cache-Control (Cloudflare), Surrogate-Control (Fastly), or configuration-based overrides (CloudFront). These allow edge-specific caching without affecting browser behavior. Check your CDN's documentation for supported extensions.
In practice, Cache-Control headers combine multiple directives to express complete caching policies. Here are battle-tested patterns for common scenarios.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
# ═══════════════════════════════════════════════════════════# STATIC ASSETS WITH CONTENT HASH# ═══════════════════════════════════════════════════════════# Files like: app.abc123.js, styles.def456.css, image.ghi789.pngCache-Control: public, max-age=31536000, immutable# Cache forever, never revalidate (URL changes when content changes) # ═══════════════════════════════════════════════════════════# VERSIONED ASSETS WITHOUT HASH# ═══════════════════════════════════════════════════════════# Files like: /v2/api.js, /2024/styles.cssCache-Control: public, max-age=2592000# 30 days; version in path provides cache-busting # ═══════════════════════════════════════════════════════════# UNVERSIONED ASSETS (careful!)# ═══════════════════════════════════════════════════════════# Files like: /logo.png, /favicon.icoCache-Control: public, max-age=86400, stale-while-revalidate=604800# 1 day fresh, then stale+refresh for 1 week# Use URL versioning (?v=2) for updates # ═══════════════════════════════════════════════════════════# HTML PAGES (PUBLIC)# ═══════════════════════════════════════════════════════════Cache-Control: public, max-age=60, stale-while-revalidate=3600, stale-if-error=86400# Fresh for 1 min, stale+refresh for 1 hour, error fallback for 1 day # ═══════════════════════════════════════════════════════════# HTML PAGES (PERSONALIZED/AUTHENTICATED)# ═══════════════════════════════════════════════════════════Cache-Control: private, no-cache# Browser may cache, must revalidate every request# Prevents CDN caching of user-specific content # ═══════════════════════════════════════════════════════════# API - CACHEABLE READ ENDPOINTS# ═══════════════════════════════════════════════════════════Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=60# Browser: 1 min, CDN: 5 min, SWR for 1 min after expiry # ═══════════════════════════════════════════════════════════# API - USER-SPECIFIC ENDPOINTS# ═══════════════════════════════════════════════════════════Cache-Control: private, max-age=0, must-revalidate# Browser caches for validation (ETag), no CDN caching # ═══════════════════════════════════════════════════════════# API - WRITE ENDPOINTS (POST/PUT/DELETE)# ═══════════════════════════════════════════════════════════Cache-Control: no-store# Never cache mutations # ═══════════════════════════════════════════════════════════# SENSITIVE DATA (BANKING, MEDICAL, PII)# ═══════════════════════════════════════════════════════════Cache-Control: no-store# Plus additional security headers:# Pragma: no-cache (HTTP/1.0 fallback)# Expires: 0 # ═══════════════════════════════════════════════════════════# REAL-TIME DATA (STOCK PRICES, LIVE SCORES)# ═══════════════════════════════════════════════════════════Cache-Control: public, max-age=0, s-maxage=10, stale-while-revalidate=5# Very short TTL, instant SWR# Consider WebSocket instead if true real-time neededWhen uncertain, start with shorter TTLs and more restrictive caching. It's easier to increase cache TTLs after verifying correctness than to debug stale content issues. You can always make caching more aggressive once you've validated the behavior.
Cache-Control is the foundation of HTTP caching. Mastering its directives enables you to express precise caching behavior across all layers of your architecture.
public, private, and no-store determine what CAN be cached.max-age for all caches, s-maxage for CDNs only.no-cache, must-revalidate, and immutable control validation behavior.stale-while-revalidate and stale-if-error provide graceful degradation.What's Next:
We've explored TTL configuration and Cache-Control headers—the mechanisms that define how long content stays fresh. But what happens when content expires? The next page covers stale-while-revalidate in depth—the powerful pattern that eliminates latency spikes while maintaining content freshness.
You now have comprehensive knowledge of Cache-Control headers—every directive, its purpose, and how to combine them for any caching scenario. This is essential knowledge for any engineer working with CDNs, reverse proxies, or web performance optimization.