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.
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:
- Freshness — Is the cached copy still valid? Determined by
Cache-Control: max-ageorExpires. - Validation — If stale, can we confirm with the server that the content hasn't changed? Determined by
ETagorLast-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-cachedoes NOT mean "don't cache." It means "cache it, but always check if it's still valid." Useno-storeif 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: CookieorVary: Authorizationeffectively 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-maxagelets you set different TTLs for the CDN vs. browser:Cache-Control: public, max-age=60, s-maxage=86400Browser 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:
- Make a request and check the
Cache-Control,ETag, andLast-Modifiedheaders - Make the same request again — check for
Ageheader (seconds since cached) andX-Cache: HIT - Check
CF-Cache-Status(Cloudflare) orX-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-cacheor very shortmax-age - API responses cached based on update frequency
- Sensitive data uses
no-store - ETags or
Last-Modifiedenabled for conditional requests -
stale-while-revalidateon 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.