Proper caching headers dramatically improve performance. This guide covers HTTP caching mechanisms.
Cache-Control Directives#
Common Directives#
Cache-Control: public # Cacheable by anyone
Cache-Control: private # Only browser can cache
Cache-Control: no-cache # Must revalidate
Cache-Control: no-store # Never cache
Cache-Control: max-age=3600 # Cache for 1 hour
Cache-Control: s-maxage=86400 # CDN cache for 1 day
Cache-Control: stale-while-revalidate=60 # Serve stale while refreshing
Cache-Control: immutable # Never changes
Combined Directives#
1// Static assets with hash in filename
2res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
3
4// API responses
5res.setHeader('Cache-Control', 'private, max-age=60, stale-while-revalidate=300');
6
7// User-specific data
8res.setHeader('Cache-Control', 'private, no-cache');
9
10// Sensitive data
11res.setHeader('Cache-Control', 'no-store');ETags for Validation#
1import crypto from 'crypto';
2
3function generateETag(content: string): string {
4 return `"${crypto.createHash('md5').update(content).digest('hex')}"`;
5}
6
7app.get('/api/data', async (req, res) => {
8 const data = await fetchData();
9 const content = JSON.stringify(data);
10 const etag = generateETag(content);
11
12 // Check If-None-Match header
13 if (req.headers['if-none-match'] === etag) {
14 return res.status(304).end();
15 }
16
17 res.setHeader('ETag', etag);
18 res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
19 res.json(data);
20});Last-Modified#
1app.get('/api/posts/:id', async (req, res) => {
2 const post = await getPost(req.params.id);
3 const lastModified = post.updatedAt.toUTCString();
4
5 // Check If-Modified-Since
6 const ifModifiedSince = req.headers['if-modified-since'];
7 if (ifModifiedSince && new Date(ifModifiedSince) >= post.updatedAt) {
8 return res.status(304).end();
9 }
10
11 res.setHeader('Last-Modified', lastModified);
12 res.setHeader('Cache-Control', 'private, max-age=60');
13 res.json(post);
14});Vary Header#
// Cache varies by these headers
res.setHeader('Vary', 'Accept-Encoding, Accept-Language');
// For API versioning
res.setHeader('Vary', 'Accept, Authorization');Next.js Caching#
1// App Router
2export const revalidate = 3600; // Revalidate every hour
3
4// Route handlers
5export async function GET() {
6 return Response.json(data, {
7 headers: {
8 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
9 },
10 });
11}Common Patterns#
Static Assets#
location ~* \.(js|css|png|jpg|webp|woff2)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}HTML Pages#
location ~* \.html$ {
add_header Cache-Control "no-cache";
}API Responses#
// Public data
res.setHeader('Cache-Control', 's-maxage=60, stale-while-revalidate=600');
// User-specific data
res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');Debugging#
# Check cache headers
curl -I https://example.com/api/data
# Force fresh request
curl -H "Cache-Control: no-cache" https://example.com/api/dataSet appropriate TTLs, use ETags for validation, and leverage stale-while-revalidate for better UX.