Proper cache configuration reduces server load and speeds up website loading. Browsers and CDNs use HTTP headers to decide whether to store a response in the cache, how long to keep it, and when to request an update. Incorrect settings can result in stale content or, conversely, unnecessary requests.
You can check the current caching headers with HTTP Headers Analyzer and use Web Vitals to assess their impact on performance.
How HTTP caching works
- The server returns a response with the headers
Cache-Control,ETag,Expires. - The browser stores the response in its cache.
- On a repeat request, the browser checks whether the cache has expired.
- If it has expired, the browser sends a conditional request with
If-None-Match(ETag) orIf-Modified-Since. - The server responds with 304 Not Modified or returns new content.
For more details on all HTTP headers, see HTTP headers: a complete reference for developers.
Cache-Control: key directives
Cache lifetime
Cache-Control: max-age=3600max-age — the time, in seconds, that the response is considered fresh. After it expires, the browser may send a conditional request for validation.
Public and private caches
Cache-Control: public, max-age=86400| Value | Description |
|---|---|
| public | Can be cached by a CDN, proxy, or browser |
| private | Only in the user's browser (personal data) |
Disabling caching
Cache-Control: no-storeThe response is not stored. Use this for sensitive data (banking transactions, personal information).
Cache-Control: no-cacheThe cache can store the response, but the server must validate it before use (using ETag or Last-Modified).
CDN and shared cache
Cache-Control: public, max-age=3600, s-maxage=86400s-maxage — the lifetime in a shared cache (CDN, proxy). The browser uses max-age.
Stale-while-revalidate
Cache-Control: max-age=3600, stale-while-revalidate=600While revalidation is in progress (for up to 600 seconds), the browser can serve stale content. This improves perceived performance.
ETag and conditional requests
An ETag is a hash or identifier for a content version. On a repeat request, the browser sends:
If-None-Match: "abc123"If the content has not changed, the server responds:
HTTP/1.1 304 Not Modified
ETag: "abc123"The response body is not sent, saving bandwidth.
Weak ETag
ETag: W/"abc123"W/ means a weak ETag: the content is semantically equivalent but may differ byte-for-byte (for example, in whitespace).
Last-Modified and If-Modified-Since
An alternative to ETag is the date of the last modification:
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMTOn a repeat request:
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMTThe server compares the date and, if there have been no changes, returns 304.
Expires (deprecated mechanism)
Expires: Thu, 31 Dec 2025 23:59:59 GMTProblems: it depends on the accuracy of the client's clock and is inflexible. Prefer using Cache-Control: max-age.
Vary
Vary: Accept-Encoding, User-AgentIndicates that the response depends on these request headers. The cache stores separate variants for different combinations. This is important for content that depends on language, device, or encoding.
Strategies by content type
Static assets with a hash in the filename
Files such as main.a1b2c3d4.js, style.e5f6g7h8.css — the filename changes when the content changes. They can be cached for a long time:
Cache-Control: public, max-age=31536000, immutableStatic assets without a hash
Cache-Control: public, max-age=86400, must-revalidateA short cache lifetime with mandatory validation after expiration.
HTML pages
Cache-Control: no-cacheOr max-age=0, must-revalidate — always validate before use. HTML often contains links to current versions of JS/CSS.
APIs with personal data
Cache-Control: private, no-storeAPIs with public data
Cache-Control: public, max-age=60, stale-while-revalidate=300Configuration in Nginx
# Статика с хешем
location ~* \.[a-f0-9]{8,}\.(js|css)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Обычная статика
location /static/ {
add_header Cache-Control "public, max-age=86400, must-revalidate";
}
# HTML
location / {
add_header Cache-Control "no-cache";
}Configuration in Node.js (Express)
// Статика
app.use('/static', express.static('public', {
maxAge: '1y',
immutable: true,
etag: true,
lastModified: true
}));
// HTML
app.use((req, res, next) => {
if (req.path.endsWith('.html') || !req.path.includes('.')) {
res.setHeader('Cache-Control', 'no-cache');
}
next();
});Checking caching
curl -I https://example.com/static/main.jsExpected headers:
Cache-Control: public, max-age=31536000, immutable
ETag: "abc123"Online: HTTP Headers — a complete analysis of response headers.
Common mistakes
- Long cache lifetime for HTML without hashes in URLs — users see an old version after an update.
- Using no-cache instead of no-store — for sensitive data, use
no-store. - Ignoring Vary — incorrect content for different Accept-Language or User-Agent values.
- Caching authenticated pages — personal data can leak into a shared cache.