Developer Tools

HTTP Caching Guide: Speed Up Your Website Without Extra Infrastructure

Master Cache-Control headers, ETags, CDN caching strategies, and browser cache behavior to dramatically improve your site's load time and reduce server costs.

7 min read

Server racks in a data center

Caching is the single highest-leverage performance optimization you can make. A cached response is served in microseconds, costs nothing, and requires zero server processing. Yet most applications either cache too aggressively (serving stale content) or not at all (wasting bandwidth and compute). Understanding HTTP caching turns it from a source of bugs into a superpower.

How HTTP caching works

When a browser or CDN receives a response, it checks the headers to decide whether and how long to cache it. On the next request for the same resource, it can serve the cached copy — without touching your server at all.

The cache lifecycle has two phases:

  1. Freshness — Is the cached copy still valid? Determined by Cache-Control: max-age or Expires.
  2. Validation — If stale, can we confirm with the server that the content hasn't changed? Determined by ETag or Last-Modified.

Cache-Control: the primary caching directive

Cache-Control is the most powerful caching header. It's a comma-separated list of directives:

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

Key directives

Directive Meaning
public Any cache (browser, CDN, proxy) can store this
private Only the end user's browser can cache (not CDNs)
no-cache Must revalidate with server before each use (not "don't cache")
no-store Never cache at all — for sensitive data
max-age=N Cache for N seconds
s-maxage=N CDN-specific max age (overrides max-age for shared caches)
immutable Content will never change — skip revalidation entirely
must-revalidate When stale, must revalidate before serving
stale-while-revalidate=N Serve stale for N seconds while fetching fresh in background

⚠️ no-cache does NOT mean "don't cache." It means "cache it, but always check if it's still valid." Use no-store if you truly never want something cached.

Caching strategy by resource type

Different resources need different strategies:

Static assets with content hashing (CSS, JS, images)

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

If your build tool adds a hash to filenames (main.a3f9b2c.js), the URL changes when content changes. Cache forever — any new version gets a new URL.

HTML pages

Cache-Control: no-cache

Or with a short TTL:

Cache-Control: public, max-age=60, stale-while-revalidate=3600

HTML changes frequently and links to the hashed assets. Cache briefly or force revalidation.

API responses

# Public data (e.g., product catalog)
Cache-Control: public, max-age=300, stale-while-revalidate=600

# User-specific data
Cache-Control: private, max-age=60

# Real-time data (stock prices, live scores)
Cache-Control: no-store

Sensitive data (authentication, payment)

Cache-Control: no-store

Never cache. Period.

ETags and conditional requests

When a cached response expires, the browser doesn't just discard it — it asks the server if it's still valid. This is revalidation.

ETags

An ETag is a fingerprint of the response content:

# Server sends:
ETag: "a3f9b2c8d4e1"

# Browser's next request:
If-None-Match: "a3f9b2c8d4e1"

# If unchanged, server responds:
HTTP/1.1 304 Not Modified
(no body — saves bandwidth)

# If changed, server responds:
HTTP/1.1 200 OK
ETag: "b7c2d4e9a1f3"
(full new response)

A 304 Not Modified response has no body — just headers. This saves all the bandwidth of re-downloading the content.

Last-Modified

Similar but uses a timestamp instead of a hash:

Last-Modified: Tue, 01 Apr 2026 10:00:00 GMT

# Browser sends:
If-Modified-Since: Tue, 01 Apr 2026 10:00:00 GMT

ETags are more reliable (timestamps can be imprecise with load-balanced servers).

Vary header: cache per request variant

The Vary header tells caches which request headers affect the response:

Vary: Accept-Encoding

This caches a separate copy for gzip and br responses. Common uses:

Vary: Accept-Encoding          # Separate caches for compressed/uncompressed
Vary: Accept-Language          # Separate caches per language
Vary: Accept                   # Separate caches for JSON vs HTML responses

⚠️ Vary: Cookie or Vary: Authorization effectively disables CDN caching — CDNs can't cache user-specific responses.

stale-while-revalidate: background refresh

One of the most useful modern caching patterns:

Cache-Control: max-age=60, stale-while-revalidate=600
  • Serve instantly from cache for the first 60 seconds (fresh)
  • For requests between 60–660 seconds: serve the stale copy immediately, but fetch a fresh version in the background
  • After 660 seconds: must revalidate before serving

Users always get a fast response. The cache stays fresh without forcing anyone to wait for a network round-trip.

CDN caching considerations

CDNs (Cloudflare, CloudFront, Fastly) respect Cache-Control headers but add their own layer of complexity:

  • s-maxage lets you set different TTLs for the CDN vs. browser:

    Cache-Control: public, max-age=60, s-maxage=86400
    

    Browser caches for 1 minute; CDN caches for 24 hours.

  • Cache purging — when you deploy, purge the CDN cache for updated assets. Most CDNs offer API-based purging.

  • Cache keys — CDNs cache based on URL + Vary headers. Query strings are usually included in the cache key.

Testing cache behavior

Use our API Request Builder to inspect response headers and verify your caching setup is working:

  1. Make a request and check the Cache-Control, ETag, and Last-Modified headers
  2. Make the same request again — check for Age header (seconds since cached) and X-Cache: HIT
  3. Check CF-Cache-Status (Cloudflare) or X-Cache (CloudFront) to confirm CDN caching

In Chrome DevTools → Network tab → click a resource → Headers tab — look for from disk cache or from memory cache in the response.

Nginx caching configuration

Configure caching headers at the Nginx level for consistent behavior:

# Static assets — cache forever
location ~* \.(js|css|woff2|png|jpg|webp|svg)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

# HTML — revalidate always
location ~* \.html$ {
    add_header Cache-Control "no-cache";
}

# API — short cache with stale-while-revalidate
location /api/ {
    add_header Cache-Control "public, max-age=60, stale-while-revalidate=600";
}

Use our Nginx Config Generator to generate a complete, optimized Nginx configuration for your use case.

Caching checklist

  • Static assets (CSS/JS) use content-hashed filenames + max-age=31536000, immutable
  • HTML served with no-cache or very short max-age
  • API responses cached based on update frequency
  • Sensitive data uses no-store
  • ETags or Last-Modified enabled for conditional requests
  • stale-while-revalidate on appropriate API endpoints
  • CDN caching headers verified and tested

Proper HTTP caching makes your site feel instant for repeat visitors, cuts your bandwidth costs, and reduces server load — all without any extra infrastructure.