HTTP

Guide

HTTP Headers and Caching: A Practical Guide

Master HTTP caching with Cache-Control, ETag, Last-Modified, and conditional request headers. Learn how to optimize performance with proper cache strategies.

6 min read intermediate Try in Playground

TL;DR: HTTP caching uses Cache-Control headers to specify how long resources should be cached and ETag/If-None-Match for validation. Proper caching reduces load times by 50-90% and decreases server load.

HTTP caching is one of the most powerful performance optimization techniques available to web developers. By storing frequently requested resources closer to users and avoiding unnecessary data transfers, caching can reduce load times by 50-90% and significantly decrease server load. This guide explores how HTTP headers control caching behavior, from initial storage decisions to cache validation and invalidation.

Introduction

Caching works by storing copies of resources at various points between the client and server. When a cached resource is requested again, it can be served immediately without contacting the origin server. However, this introduces a fundamental challenge: how do we ensure cached content remains fresh and accurate?

HTTP solves this through a sophisticated system of cache control headers and validation mechanisms. These headers allow servers to specify exactly how long resources should be cached, under what conditions they can be reused, and how to check if cached content is still valid.

Understanding caching is crucial because it affects every aspect of web performance: page load times, bandwidth usage, server costs, and user experience. A well-designed caching strategy can make the difference between a fast, responsive application and one that feels sluggish and expensive to operate.

Cache Control Fundamentals

Cache-Control Directives

The Cache-Control header is the primary mechanism for controlling caching behavior. It uses directives that specify how, where, and for how long resources should be cached.

Common Cache-Control Directives:

Cache-Control: max-age=3600
Cache-Control: no-cache
Cache-Control: no-store
Cache-Control: public, max-age=86400
Cache-Control: private, max-age=300, must-revalidate
```text

**Directive Meanings:**

- `max-age=N`: Cache for N seconds from response time
- `public`: Can be cached by any cache (browsers, CDNs, proxies)
- `private`: Only cacheable by browser, not shared caches
- `no-cache`: Must revalidate with server before using cached copy
- `no-store`: Never cache this response
- `must-revalidate`: Must revalidate when cache expires

### Cache Storage Locations

**Browser Cache:**

```http
Cache-Control: private, max-age=300

Stores resources locally on user’s device. Fast access but limited to single user.

CDN/Proxy Cache:

Cache-Control: public, max-age=86400
```text

Shared caches that serve multiple users. Excellent for static assets.

**Application Cache:**

```http
Cache-Control: no-cache, private

Server-side caching (Redis, Memcached) for dynamic content generation.

ETag Generation and Validation

ETags (Entity Tags) provide a mechanism for cache validation without transferring the entire resource. They’re essentially fingerprints that change when content changes.

ETag Types

Strong ETags:

ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
```text

Indicates byte-for-byte identical content. Changes with any modification.

**Weak ETags:**

```http
ETag: W/"33a64df551425fcc55e4d42a148795d9f25f89d4"

Indicates semantically equivalent content. May not change for minor modifications.

ETag Generation Strategies

Content-Based ETags:

// Generate ETag from content hash
const crypto = require('crypto')
const content = JSON.stringify(userData)
const etag = crypto.createHash('md5').update(content).digest('hex')

response.setHeader('ETag', `"${etag}"`)
```javascript

**Timestamp-Based ETags:**

```javascript
// Generate ETag from last modified time
const lastModified = new Date(user.updatedAt)
const etag = lastModified.getTime().toString(16)

response.setHeader('ETag', `"${etag}"`)

Version-Based ETags:

// Generate ETag from resource version
const etag = `"v${user.version}"`
response.setHeader('ETag', etag)
```text

## Conditional Request Validation

Conditional requests allow clients to validate cached content without downloading it again. This saves bandwidth and improves performance.

### If-None-Match Validation

The most common validation mechanism uses ETags with the `If-None-Match` header.

**Client Request:**

```http
GET /api/users/123 HTTP/1.1
Host: api.example.com
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

Server Response (Content Unchanged):

HTTP/1.1 304 Not Modified
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Cache-Control: max-age=300
Content-Length: 0
```text

**Server Response (Content Changed):**

```http
HTTP/1.1 200 OK
ETag: "7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730"
Cache-Control: max-age=300
Content-Type: application/json

{
  "id": 123,
  "name": "Alice Johnson",
  "email": "alice.johnson@example.com",
  "updatedAt": "2026-01-18T10:30:00Z"
}

If-Modified-Since Validation

An alternative validation mechanism using timestamps.

Client Request:

GET /api/users/123 HTTP/1.1
Host: api.example.com
If-Modified-Since: Fri, 18 Jan 2026 09:30:00 GMT
```text

**Server Response (Not Modified):**

```http
HTTP/1.1 304 Not Modified
Last-Modified: Fri, 18 Jan 2026 09:30:00 GMT
Cache-Control: max-age=300

Cache Lifecycle Examples

Fresh Cache Scenario

1. Initial Request:
   GET /api/products
   → 200 OK, Cache-Control: max-age=600, ETag: "abc123"

2. Subsequent Request (within 10 minutes):
   GET /api/products
   → Served from cache (no network request)

3. Cache Status: FRESH
   Time remaining: 400 seconds

Stale Cache with Successful Validation

1. Cache Expired:
   Cached response age: 650 seconds (> max-age=600)

2. Validation Request:
   GET /api/products
   If-None-Match: "abc123"

3. Server Response:
   304 Not Modified, ETag: "abc123"

4. Cache Status: FRESH (revalidated)
   New expiration: current_time + 600 seconds

Stale Cache with Failed Validation

1. Cache Expired:
   Cached response age: 650 seconds

2. Validation Request:
   GET /api/products
   If-None-Match: "abc123"

3. Server Response:
   200 OK, ETag: "def456", [new content]

4. Cache Status: FRESH (updated)
   Old cache entry replaced with new response

Cache Invalidation Strategies

Time-Based Invalidation

Short TTL for Dynamic Content:

Cache-Control: max-age=60, must-revalidate
```text

**Long TTL for Static Assets:**

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

Event-Based Invalidation

Cache Purging:

// Purge specific URLs from CDN
await cdn.purge(['/api/users/123', '/api/users/123/profile'])
```http

**Cache Tags:**

```http
Cache-Control: max-age=3600
Cache-Tags: user:123, profile, api

Versioned URLs

Asset Versioning:

<!-- Old version -->
<link rel="stylesheet" href="/styles.css?v=1.2.3" />

<!-- New version (cache miss) -->
<link rel="stylesheet" href="/styles.css?v=1.2.4" />
```text

## Advanced Caching Patterns

### Stale-While-Revalidate

Serve stale content immediately while updating cache in background.

```http
Cache-Control: max-age=300, stale-while-revalidate=86400

Behavior:

  • 0-300 seconds: Serve from cache
  • 300-86700 seconds: Serve stale content, trigger background update
  • 86700+ seconds: Must revalidate before serving

Vary Header for Content Negotiation

Cache different versions based on request headers.

Vary: Accept-Encoding, Accept-Language
Cache-Control: public, max-age=3600
```javascript

**Cache Keys:**

- `/api/users` + `Accept-Encoding: gzip` + `Accept-Language: en`
- `/api/users` + `Accept-Encoding: br` + `Accept-Language: es`

## Implementation Examples

### Express.js Caching Middleware

```javascript
function cacheMiddleware(maxAge = 300) {
  return (req, res, next) => {
    // Generate ETag from response content
    const originalSend = res.send

    res.send = function (data) {
      const etag = generateETag(data)

      res.set({
        ETag: etag,
        'Cache-Control': `max-age=${maxAge}`,
        'Last-Modified': new Date().toUTCString()
      })

      // Check if client has current version
      if (req.headers['if-none-match'] === etag) {
        return res.status(304).end()
      }

      originalSend.call(this, data)
    }

    next()
  }
}

CDN Cache Configuration

// Cloudflare Workers example
addEventListener('fetch', (event) => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  const cache = caches.default
  const cacheKey = new Request(request.url, request)

  // Check cache first
  let response = await cache.match(cacheKey)

  if (!response) {
    // Fetch from origin
    response = await fetch(request)

    // Cache based on content type
    if (response.headers.get('content-type')?.includes('application/json')) {
      response.headers.set('Cache-Control', 'max-age=300')
    }

    // Store in cache
    event.waitUntil(cache.put(cacheKey, response.clone()))
  }

  return response
}
```text

## Best Practices

### Cache Strategy Selection

**Static Assets:**

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

Long-term caching with versioned URLs.

API Responses:

Cache-Control: private, max-age=300, must-revalidate
```text

Short-term caching with validation.

**User-Specific Content:**

```http
Cache-Control: private, no-cache

Always validate before serving.

Performance Monitoring

Cache Hit Ratio:

const cacheHitRatio = cacheHits / (cacheHits + cacheMisses)
// Target: > 80% for static content, > 60% for dynamic content
```javascript

**Cache Validation Efficiency:**

```javascript
const validationSuccessRate = notModifiedResponses / validationRequests
// Target: > 70% for stable content

Common Pitfalls

Over-Caching Dynamic Content:

// Bad: User data cached too long
Cache-Control: max-age=3600

// Good: Short cache with validation
Cache-Control: max-age=60, must-revalidate
```text

**Under-Caching Static Assets:**

```http
// Bad: CSS/JS files expire quickly
Cache-Control: max-age=300

// Good: Long-term caching with versioning
Cache-Control: public, max-age=31536000, immutable

HTTP caching integrates with many other web technologies:

Effective caching requires understanding the interplay between these headers and the specific needs of your application. Start with conservative cache times and gradually optimize based on your content update patterns and performance requirements.

Frequently Asked Questions

How does HTTP caching work?

Servers send Cache-Control headers telling browsers and CDNs how long to cache responses. Cached responses are reused without contacting the server.

What is the difference between no-cache and no-store?

no-cache allows caching but requires revalidation before use. no-store prevents any caching. Use no-store for sensitive data, no-cache for frequently updated content.

What is cache revalidation?

Revalidation checks if cached content is still valid using ETag or Last-Modified. If unchanged, server returns 304 Not Modified, saving bandwidth.

How do I cache static assets effectively?

Use long max-age (1 year) with versioned filenames (app.v2.js). When content changes, change the filename. This enables aggressive caching with instant updates.

Keep Learning