HTTP

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.

6 min read intermediate Try in Playground

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 cache
  • max-age=31536000 - Cache for 1 year
  • immutable - 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 caches
  • max-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 private directive
  • Fastest cache (no network)

CDN/Proxy Cache

  • Shared cache for multiple users
  • Only stores public responses
  • 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:

  1. Make a GET request to /posts/1
  2. Check the response Cache-Control header
  3. Make the same request again
  4. Notice the faster response (from cache)
  • ETag - Resource version identifier
  • Last-Modified - Resource modification date
  • Expires - Legacy expiration header
  • Vary - Cache variation keys
  • Age - Time in cache

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.

Keep Learning