HTTP Cache Headers — Cache-Control, ETag, and Last-Modified Explained
Master HTTP caching headers: Cache-Control directives (max-age, no-cache, immutable), ETag for conditional requests, and Last-Modified. Learn how to cache static assets forever...
HTTP caching eliminates redundant requests. A correctly cached website can serve returning visitors with zero network requests. A miscached one shows stale content for days.
Look up HTTP status codes with the HTTP Status Codes reference.
How HTTP caching works
First request:
Client → GET /style.css
Server → 200 OK, Cache-Control: max-age=31536000
Cache stores: style.css + expiration date
Second request (within max-age):
Client uses cache → no network request
After expiration:
Client → GET /style.css, If-None-Match: "abc123"
Server → 304 Not Modified (file unchanged, use cache)
OR
Server → 200 OK (file changed, here's the new one)
Cache-Control directives
# max-age: seconds until cache expires
Cache-Control: max-age=86400 # 1 day
Cache-Control: max-age=31536000 # 1 year
# no-cache: must revalidate before using cache
Cache-Control: no-cache # Revalidate every request (304 if unchanged)
# no-store: never cache
Cache-Control: no-store # Sensitive data (health records, banking)
# public: CDNs and proxies can cache
Cache-Control: public, max-age=86400
# private: only the browser caches (not CDNs)
Cache-Control: private, max-age=3600 # Personalized content
# immutable: file will NEVER change, skip revalidation
Cache-Control: public, max-age=31536000, immutable
# stale-while-revalidate: serve stale while fetching new in background
Cache-Control: max-age=60, stale-while-revalidate=86400
ETag and conditional requests
ETag is a fingerprint of the resource content. Browsers send it back with If-None-Match to check if the file changed:
# Server sends ETag with response:
HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Cache-Control: no-cache
# Browser sends ETag on next request:
GET /api/data HTTP/1.1
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# If unchanged:
HTTP/1.1 304 Not Modified
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# No body — browser uses its cached copy
# If changed:
HTTP/1.1 200 OK
ETag: "new-hash-value"
Content-Type: application/json
{ ...new data... }
ETags are ideal for API responses that change infrequently but need to be fresh when they do.
Last-Modified
Older alternative to ETag, using timestamps:
# Server sends Last-Modified:
HTTP/1.1 200 OK
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
Cache-Control: no-cache
# Browser sends If-Modified-Since on next request:
GET /image.jpg HTTP/1.1
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT
# If unchanged:
HTTP/1.1 304 Not Modified
Prefer ETags over Last-Modified — timestamps have 1-second resolution and can cause issues when files are regenerated without content changes.
Strategy: static assets vs HTML
The correct strategy is different for static assets and HTML pages:
# Static assets (JS, CSS, images) — hash in filename, cache forever:
# /static/app.3f8c2d1a.js
Cache-Control: public, max-age=31536000, immutable
# HTML pages — never cache (always fetch fresh):
# /index.html
Cache-Control: no-cache
# Or:
Cache-Control: no-store
Why? Build tools (webpack, Vite, esbuild) add content hashes to filenames. The HTML changes to reference the new hashed filename. So:
- HTML: always fresh, but small and fast to fetch
- Assets: cached forever, never re-downloaded unless HTML references a new hash
Express: set cache headers
import express from 'express';
import { createHash } from 'crypto';
import { readFileSync } from 'fs';
const app = express();
// Static assets: cache 1 year
app.use('/static', express.static('public/static', {
maxAge: '1y',
immutable: true,
}));
// HTML: no cache
app.get('/', (req, res) => {
res.set('Cache-Control', 'no-cache');
res.sendFile('public/index.html');
});
// API: ETag-based caching
app.get('/api/config', (req, res) => {
const data = { version: '1.2.3', features: ['a', 'b'] };
const json = JSON.stringify(data);
const etag = createHash('md5').update(json).digest('hex');
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.set({
'ETag': etag,
'Cache-Control': 'no-cache',
});
res.json(data);
});
nginx cache header configuration
server {
# HTML — no cache
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# Hashed assets — cache 1 year
location ~* \.(js|css|woff2)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Images — cache 30 days
location ~* \.(jpg|jpeg|png|gif|svg|ico|webp)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
# API — ETag (nginx generates automatically for static files)
location /api/ {
etag on;
expires -1;
add_header Cache-Control "no-cache";
}
}
Vary header: cache per Accept-Encoding
# Server compresses with gzip:
HTTP/1.1 200 OK
Content-Encoding: gzip
Vary: Accept-Encoding
Cache-Control: public, max-age=86400
# The Vary header tells CDNs to cache separate copies per encoding.
# Without Vary: CDN might serve gzip to a client that doesn't support it.
stale-while-revalidate: smooth cache updates
# Serve immediately from cache, revalidate in background
Cache-Control: max-age=60, stale-while-revalidate=3600
# For 60s: serve from cache, no revalidation
# 60s–3660s: serve stale from cache, fetch new in background
# After 3660s: must wait for revalidation before serving
This pattern keeps fast page loads while ensuring content updates.
Related tools
- HTTP Status Codes — 304 Not Modified and other status codes
- HTML Minifier — compress HTML before caching
- Hash Generator — generate ETags manually
Related posts
- When to Use 422 vs 400 (and Other HTTP Status Code Debates) — 400 means the request is malformed. 422 means it's valid but semantically wrong.…
- HTTP 301 vs 302 Redirect — Permanent vs Temporary Redirects Explained — Understand when to use 301 (permanent) vs 302 (temporary) redirects, how they af…
- HTTP 404 Not Found — What It Means and How to Fix It — HTTP 404 means the server understood the request but can't find the resource. It…
- URL Canonicalization — Normalize URLs for APIs, Caching, and SEO — URL canonicalization normalizes URLs to a consistent form, removing redundant pa…
Related tool
Full HTTP status code reference with explanations and when to use each.
Written by Mian Ali Khalid. Part of the Dev Productivity pillar.