- Home
- HTTP Headers
- Cache-Control Header: Complete HTTP Caching Guide
Header
Cache-Control Header: Complete HTTP Caching Guide
Master the Cache-Control header. Learn how to control browser and CDN caching with max-age, no-cache, no-store, and other directives.
TL;DR: Controls HTTP caching behavior with directives like max-age, no-cache, and public/private. Essential for performance optimization and proper cache management.
Cache-Control is the primary header for controlling HTTP caching. It tells browsers and CDNs whether to cache responses, for how long, and when to revalidate. Proper caching dramatically improves performance and reduces server load.
What is Cache-Control?
Cache-Control contains directives that specify caching behavior:
Cache-Control: max-age=3600, public
```text
This tells caches: "Store this response for 1 hour, and any cache can store it."
## How Caching Works
```text
Request → Check Cache → Cache Hit? → Return Cached Response
↓ No
Fetch from Server → Store in Cache → Return Response
```text
Without caching, every request goes to the server. With caching, repeated requests are served instantly from local storage.
## Cache-Control Directives
### Response Directives
| Directive | Meaning |
| -------------------------- | ---------------------------------------- |
| `max-age=N` | Cache for N seconds |
| `s-maxage=N` | Cache for N seconds (shared caches only) |
| `no-cache` | Cache but revalidate before use |
| `no-store` | Never cache |
| `public` | Any cache can store |
| `private` | Only browser can cache |
| `must-revalidate` | Must revalidate when stale |
| `immutable` | Never changes, don't revalidate |
| `stale-while-revalidate=N` | Serve stale while fetching fresh |
| `stale-if-error=N` | Serve stale if server errors |
### Request Directives
| Directive | Meaning |
| ------------- | ---------------------------------------------- |
| `no-cache` | Don't use cached response without revalidation |
| `no-store` | Don't store the response |
| `max-age=N` | Accept cached response up to N seconds old |
| `max-stale=N` | Accept stale response up to N seconds |
| `min-fresh=N` | Response must be fresh for at least N seconds |
## Common Patterns
### Static Assets (Long Cache)
For files with versioned URLs (e.g., `app.a1b2c3.js`):
```http
Cache-Control: public, max-age=31536000, immutable
public- CDNs can cachemax-age=31536000- Cache for 1 yearimmutable- Never revalidate (URL changes when content changes)
HTML Pages (Revalidate)
For HTML that might change:
Cache-Control: no-cache
ETag: "abc123"
```text
- `no-cache` - Always check with server
- `ETag` - Server can return 304 if unchanged
### API Responses (Short Cache)
For data that changes occasionally:
```http
Cache-Control: private, max-age=60
private- Only user’s browser cachesmax-age=60- Fresh for 1 minute
Sensitive Data (No Cache)
For banking, medical, or personal data:
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
```text
Ensures response is never stored anywhere.
### CDN with Background Refresh
For content that should feel instant but stay fresh:
```http
Cache-Control: public, max-age=60, stale-while-revalidate=300
- Serve from cache for 60 seconds
- After 60s, serve stale while fetching fresh (up to 300s)
Directive Deep Dive
max-age
Specifies freshness lifetime in seconds:
Cache-Control: max-age=3600 # 1 hour
Cache-Control: max-age=86400 # 1 day
Cache-Control: max-age=604800 # 1 week
Cache-Control: max-age=31536000 # 1 year
```text
After max-age expires, the cached response is "stale" and should be revalidated.
### no-cache vs no-store
```http
# no-cache: Cache it, but always ask server first
Cache-Control: no-cache
→ Browser stores response
→ Before using, sends If-None-Match to server
→ Server returns 304 (use cache) or 200 (new data)
# no-store: Never cache
Cache-Control: no-store
→ Response is never stored
→ Every request goes to server
Use no-cache for content that might change but benefits from conditional requests.
Use no-store for sensitive data that must never be stored.
public vs private
# public: Any cache can store
Cache-Control: public, max-age=3600
→ Browser cache: ✅
→ CDN cache: ✅
→ Proxy cache: ✅
# private: Only browser can store
Cache-Control: private, max-age=3600
→ Browser cache: ✅
→ CDN cache: ❌
→ Proxy cache: ❌
```text
Use `private` for user-specific data (account info, personalized content).
### immutable
Tells browsers the content will never change:
```http
Cache-Control: public, max-age=31536000, immutable
Without immutable, browsers may revalidate on page reload even if max-age hasn’t expired. With immutable, they skip revalidation entirely.
Use for versioned static assets where the URL changes when content changes.
stale-while-revalidate
Improves perceived performance:
Cache-Control: max-age=60, stale-while-revalidate=300
```text
Timeline:
- 0-60s: Serve fresh from cache
- 60-360s: Serve stale immediately, fetch fresh in background
- 360s+: Must wait for fresh response
### must-revalidate
Prevents using stale responses:
```http
Cache-Control: max-age=3600, must-revalidate
After max-age expires, cache MUST revalidate before serving. Without this, caches might serve stale content if the server is unreachable.
Real-World Examples
Static Website Assets
# CSS with content hash in filename
GET /styles.a1b2c3d4.css HTTP/1.1
HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, immutable
Content-Type: text/css
```text
### API with User Data
```http
GET /api/user/profile HTTP/1.1
Authorization: Bearer token123
HTTP/1.1 200 OK
Cache-Control: private, no-cache
ETag: "user-123-v5"
Content-Type: application/json
{"id": 123, "name": "Alice"}
News Article
GET /news/breaking-story HTTP/1.1
HTTP/1.1 200 OK
Cache-Control: public, max-age=300, stale-while-revalidate=600
Content-Type: text/html
<!DOCTYPE html>...
```text
### Login Page
```http
GET /login HTTP/1.1
HTTP/1.1 200 OK
Cache-Control: no-store
Content-Type: text/html
Conditional Requests
Cache-Control works with validation headers for efficient revalidation:
ETag Validation
# First request
GET /api/data HTTP/1.1
HTTP/1.1 200 OK
Cache-Control: no-cache
ETag: "abc123"
{"data": "..."}
# Subsequent request (revalidation)
GET /api/data HTTP/1.1
If-None-Match: "abc123"
HTTP/1.1 304 Not Modified
# No body - use cached version
```text
### Last-Modified Validation
```http
# First request
GET /page.html HTTP/1.1
HTTP/1.1 200 OK
Cache-Control: no-cache
Last-Modified: Sat, 18 Jan 2026 10:00:00 GMT
<!DOCTYPE html>...
# Subsequent request
GET /page.html HTTP/1.1
If-Modified-Since: Sat, 18 Jan 2026 10:00:00 GMT
HTTP/1.1 304 Not Modified
Cache Hierarchy
Responses can be cached at multiple levels:
Browser Cache → CDN/Proxy Cache → Origin Server
↑ ↑ ↑
private public source
Browser Cache
- Stores responses locally
- Respects
privatedirective - Fastest cache (no network)
CDN/Proxy Cache
- Shared cache for multiple users
- Only stores
publicresponses - Reduces origin server load
Origin Server
- Source of truth
- Generates fresh responses
- Sets caching headers
Best Practices
Versioned Static Assets
/app.js → Cache-Control: no-cache (or short max-age)
/app.v2.js → Cache-Control: max-age=31536000, immutable
/app.a1b2c3.js → Cache-Control: max-age=31536000, immutable
Use content hashes or version numbers in filenames, then cache forever.
HTML Documents
Cache-Control: no-cache
```text
Always revalidate HTML so users get the latest version with updated asset references.
### API Responses
```http
# Public data
Cache-Control: public, max-age=300
# User-specific data
Cache-Control: private, max-age=60
# Real-time data
Cache-Control: no-store
Sensitive Data
Cache-Control: no-store, no-cache, must-revalidate, private
Pragma: no-cache
Expires: 0
```text
Belt-and-suspenders approach for maximum compatibility.
## Common Mistakes
### Caching User-Specific Data Publicly
```http
# ❌ Wrong: User data cached by CDN
Cache-Control: public, max-age=3600
# ✅ Correct: Only browser caches
Cache-Control: private, max-age=3600
Long Cache Without Versioning
# ❌ Wrong: Can't update cached file
/app.js with Cache-Control: max-age=31536000
# ✅ Correct: New URL = new cache entry
/app.a1b2c3.js with Cache-Control: max-age=31536000, immutable
```text
### Using Expires Instead of Cache-Control
```http
# ❌ Old way (still works but less flexible)
Expires: Sat, 18 Jan 2027 10:00:00 GMT
# ✅ Modern way
Cache-Control: max-age=31536000
JavaScript Cache Control
Fetch with Cache Options
// Use cache normally
fetch('/api/data')
// Skip cache, get fresh
fetch('/api/data', { cache: 'no-store' })
// Use cache but revalidate
fetch('/api/data', { cache: 'no-cache' })
// Only use cache (fail if not cached)
fetch('/api/data', { cache: 'only-if-cached' })
```javascript
### Service Worker Caching
```javascript
// Cache API for custom caching logic
const cache = await caches.open('v1')
await cache.put('/api/data', response)
const cached = await cache.match('/api/data')
Try It Yourself
Observe caching in our request builder:
- Make a GET request to
/posts/1 - Check the response Cache-Control header
- Make the same request again
- Notice the faster response (from cache)
Related Headers
- ETag - Resource version identifier
- Last-Modified - Resource modification date
- Expires - Legacy expiration header
- Vary - Cache variation keys
- Age - Time in cache
Related Concepts
- Conditional Requests - Efficient revalidation
- CDN Caching - Edge caching strategies
- Performance - Why caching matters
In Practice
Express.js
// Static assets — cache for 1 year (immutable)
app.use('/static', express.static('public', {
maxAge: '1y',
immutable: true
}))
// API response — private, revalidate every 60s
app.get('/api/profile', (req, res) => {
res.set('Cache-Control', 'private, max-age=60')
res.json(req.user)
})
// Sensitive data — never cache
app.get('/api/account/balance', (req, res) => {
res.set('Cache-Control', 'no-store')
res.json({ balance: 1234.56 })
})
// Stale-while-revalidate for public content
app.get('/api/articles', (req, res) => {
res.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=300')
res.json(articles)
})Next.js App Router
// app/api/articles/route.ts
export async function GET() {
const articles = await db.articles.findAll()
return Response.json(articles, {
headers: {
'Cache-Control': 'public, max-age=60, stale-while-revalidate=300'
}
})
}
// next.config.ts — set headers for static assets
// headers() {
// return [{ source: '/_next/static/(.*)', headers: [
// { key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }
// ]}]
// }nginx
# nginx.conf — cache static assets for 1 year
location ~* \.(js|css|png|jpg|svg|woff2)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
expires 1y;
}
# HTML — always revalidate
location ~* \.html$ {
add_header Cache-Control "no-cache";
}
# API — private, short TTL
location /api/ {
add_header Cache-Control "private, max-age=0, must-revalidate";
}Frequently Asked Questions
What is the Cache-Control header?
Cache-Control is an HTTP header that specifies caching policies for browsers and CDNs. It controls whether responses can be cached, for how long, and under what conditions they must be revalidated.
What is the difference between no-cache and no-store?
no-cache allows caching but requires revalidation with the server before each use. no-store completely prevents caching—the response must never be stored. Use no-store for sensitive data like banking information.
What does max-age mean in Cache-Control?
max-age specifies how many seconds a response can be cached before it becomes stale. For example, max-age=3600 means the response can be used from cache for 1 hour without contacting the server.
What is the difference between public and private cache?
public allows any cache (browsers, CDNs, proxies) to store the response. private restricts caching to the end user browser only—CDNs and shared caches must not store it. Use private for user-specific data.
How do I prevent caching completely?
Use Cache-Control: no-store, no-cache, must-revalidate to prevent all caching. This ensures the response is never stored and always fetched fresh from the server.
What is stale-while-revalidate?
stale-while-revalidate allows serving stale cached content while fetching a fresh version in the background. This improves perceived performance by showing cached content immediately while updating it.