View contents
Complete Guide to HTTP Caching Headers Configuration for Web Performance
Introduction
HTTP caching is one of the most powerful performance optimizations available. Properly configured caching can eliminate network requests entirely, reducing page load times from seconds to milliseconds for returning visitors. This guide provides comprehensive implementation strategies for different server environments.
New to caching? Start with our beginner-friendly guide: 📖 Read: Caching Headers Explained
Strategy 1: Cache-Control Directives Deep Dive
Complete Directive Reference
Cache-Control Directives:
┌─────────────────────────────────────────────────────────────┐
│ FRESHNESS DIRECTIVES │
│ ├── max-age=<seconds> How long cache is fresh │
│ ├── s-maxage=<seconds> CDN/proxy cache duration │
│ └── stale-while-revalidate=<seconds> Serve stale while │
│ fetching fresh copy │
│ │
│ REVALIDATION DIRECTIVES │
│ ├── no-cache Always revalidate before using │
│ ├── must-revalidate Revalidate after max-age expires │
│ └── proxy-revalidate Proxies must revalidate │
│ │
│ STORAGE DIRECTIVES │
│ ├── no-store Never cache (sensitive data) │
│ ├── private Only browser can cache │
│ └── public CDNs and proxies can cache │
│ │
│ OPTIMIZATION DIRECTIVES │
│ └── immutable Never revalidate (versioned │
│ assets) │
└─────────────────────────────────────────────────────────────┘
Combining Directives
# Versioned static assets (CSS, JS with hash in filename)
Cache-Control: public, max-age=31536000, immutable
# HTML pages (always check for updates)
Cache-Control: no-cache
# Sensitive user data (never cache)
Cache-Control: no-store, private
# API responses (short cache with background refresh)
Cache-Control: public, max-age=60, stale-while-revalidate=600
# CDN-specific longer cache
Cache-Control: public, max-age=3600, s-maxage=86400
Strategy 2: Server Configuration
Nginx Configuration
# /etc/nginx/conf.d/caching.conf
# Versioned static assets - cache forever
location ~* \.(css|js)$ {
# Only if filename contains hash (e.g., app.abc123.js)
if ($uri ~* ".*\.[a-f0-9]{8,}\.") {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Non-versioned fallback
if ($uri !~* ".*\.[a-f0-9]{8,}\.") {
add_header Cache-Control "public, max-age=86400";
}
add_header Vary "Accept-Encoding";
}
# Images and fonts - long cache
location ~* \.(jpg|jpeg|png|gif|webp|avif|svg|ico|woff|woff2|ttf|eot)$ {
add_header Cache-Control "public, max-age=31536000";
add_header Vary "Accept-Encoding";
}
# HTML - always revalidate
location ~* \.html$ {
add_header Cache-Control "no-cache";
add_header Vary "Accept-Encoding";
}
# API endpoints - short cache with stale-while-revalidate
location /api/ {
add_header Cache-Control "public, max-age=60, stale-while-revalidate=600";
add_header Vary "Accept-Encoding, Authorization";
}
# Enable ETag
etag on;
Apache Configuration
# .htaccess or httpd.conf
<IfModule mod_expires.c>
ExpiresActive On
# Default: 1 month
ExpiresDefault "access plus 1 month"
# HTML: always revalidate
ExpiresByType text/html "access plus 0 seconds"
# CSS/JS: 1 year (use versioned filenames)
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
# Images: 1 year
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
# Fonts: 1 year
ExpiresByType font/woff "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
</IfModule>
<IfModule mod_headers.c>
# Versioned assets (files with hash in name)
<FilesMatch "\.[a-f0-9]{8,}\.(css|js)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
# HTML files
<FilesMatch "\.html$">
Header set Cache-Control "no-cache"
</FilesMatch>
# Enable ETag
FileETag MTime Size
</IfModule>
Node.js/Express Configuration
const express = require('express');
const path = require('path');
const app = express();
// Custom caching middleware
const setCacheHeaders = (req, res, next) => {
const url = req.url;
// Versioned assets (contain hash)
if (/\.[a-f0-9]{8,}\.(css|js)$/.test(url)) {
res.set('Cache-Control', 'public, max-age=31536000, immutable');
}
// Images and fonts
else if (/\.(jpg|jpeg|png|gif|webp|svg|woff2?|ttf|eot)$/.test(url)) {
res.set('Cache-Control', 'public, max-age=31536000');
}
// HTML
else if (/\.html$/.test(url) || url === '/') {
res.set('Cache-Control', 'no-cache');
}
// API responses
else if (url.startsWith('/api/')) {
res.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=600');
}
next();
};
app.use(setCacheHeaders);
// Serve static files with ETags
app.use(express.static('public', {
etag: true,
lastModified: true,
maxAge: 0, // Let middleware handle Cache-Control
}));
Strategy 3: Asset Versioning (Cache Busting)
Build Tool Configuration
Webpack
// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js',
assetModuleFilename: 'assets/[name].[contenthash:8][ext]',
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css',
}),
],
};
Vite
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]',
},
},
},
});
Why Versioning Matters
Without Versioning:
├── User visits site, downloads styles.css
├── You update styles.css
├── User revisits - browser serves cached (old) styles.css
├── User sees broken layout until cache expires
└── Bad user experience!
With Versioning:
├── User visits site, downloads styles.abc123.css
├── You update CSS, build creates styles.def456.css
├── User revisits - browser requests styles.def456.css (new!)
├── User always sees latest styles
└── Great user experience!
Strategy 4: CDN Caching Configuration
Cloudflare
Page Rules:
URL: example.com/assets/*
Settings:
- Cache Level: Cache Everything
- Edge Cache TTL: 1 month
- Browser Cache TTL: 1 year
URL: example.com/*.html
Settings:
- Cache Level: Standard
- Edge Cache TTL: Respect Existing Headers
AWS CloudFront
{
"CacheBehaviors": [
{
"PathPattern": "/assets/*",
"DefaultTTL": 31536000,
"MaxTTL": 31536000,
"MinTTL": 31536000,
"Compress": true
},
{
"PathPattern": "*.html",
"DefaultTTL": 0,
"MaxTTL": 0,
"MinTTL": 0
}
]
}
Vercel
// vercel.json
{
"headers": [
{
"source": "/assets/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
},
{
"source": "/(.*).html",
"headers": [
{
"key": "Cache-Control",
"value": "no-cache"
}
]
}
]
}
Strategy 5: Cache Invalidation
Strategies Comparison
| Strategy | How It Works | Pros | Cons |
|---|---|---|---|
| Versioned URLs | Change filename | Instant, reliable | Requires build step |
| Query strings | ?v=123 | Easy to implement | Some CDNs ignore |
| CDN purge | API call to CDN | Flexible | Slow, may miss edges |
| Short TTL | Low max-age | Simple | More requests |
Recommended Approach
Best Practice Cache Invalidation:
┌─────────────────────────────────────────────────────────────┐
│ Static Assets (CSS, JS, Images): │
│ └── Use content-hash in filename (automatic invalidation) │
│ │
│ HTML Pages: │
│ └── Use no-cache (always revalidate) │
│ │
│ API Responses: │
│ └── Use stale-while-revalidate for background refresh │
│ │
│ Emergency Updates: │
│ └── CDN purge API + deploy new versioned assets │
└─────────────────────────────────────────────────────────────┘
Strategy 6: Vary Header for Conditional Caching
When to Use Vary
# Cache different versions based on encoding
Vary: Accept-Encoding
# Cache different versions for different languages
Vary: Accept-Language
# Cache different for authenticated vs anonymous users
Vary: Authorization
# Multiple conditions
Vary: Accept-Encoding, Accept-Language
Nginx Vary Configuration
# Add Vary header for compressed content
location ~* \.(css|js|html|json|xml)$ {
add_header Vary "Accept-Encoding";
gzip on;
}
# Add Vary for language-specific content
location /content/ {
add_header Vary "Accept-Language";
}
Measuring Cache Effectiveness
Before/After Comparison
BEFORE Cache Optimization:
├── Repeat Visit Page Load: 2.8s
├── Transferred Data: 1.9 MB
├── Server Requests: 42
├── Cache Hit Rate: 15%
└── Lighthouse Performance: 68
AFTER Cache Optimization:
├── Repeat Visit Page Load: 0.6s (↓79%)
├── Transferred Data: 85 KB (↓96%)
├── Server Requests: 8 (↓81%)
├── Cache Hit Rate: 94%
└── Lighthouse Performance: 96 (↑28 points)
Lighthouse Audits
Check for these results:
- “Serve static assets with an efficient cache policy” - Should pass
- Shows any resources with cache TTL < 1 week
Testing with DevTools
Network Tab Analysis:
1. Enable "Disable cache" checkbox
2. Load page (simulates first visit)
3. Uncheck "Disable cache"
4. Reload page (simulates repeat visit)
5. Compare "Size" column:
- "(disk cache)" = cached locally
- "(memory cache)" = cached in memory
- Actual size = downloaded from network
Optimization Checklist
Before deploying, verify:
- [ ] Versioned static assets use
max-age=31536000, immutable - [ ] HTML uses
no-cachefor always-fresh content - [ ] Images/fonts use
max-age=31536000 - [ ] ETags enabled for efficient revalidation
- [ ]
Vary: Accept-Encodingset for compressed content - [ ] CDN caching properly configured
- [ ] Build tool generates content hashes in filenames
- [ ] API responses use appropriate caching strategy
- [ ] Sensitive data uses
no-store - [ ] Lighthouse caching audit passes
Related Articles
Continue optimizing your resource delivery:
- Caching Headers Explained - Understand the basics
- TTFB Optimization Guide - Caching eliminates TTFB
- Text Compression Implementation Guide - Combine with caching
- Render-Blocking Resources Guide - Cache critical resources
📚 Back to Performance SEO Hub - Explore all performance topics
References
- MDN Web Docs - HTTP Caching
- web.dev - HTTP Cache
- Chrome Developers - Serve Static Assets with Efficient Cache Policy
- Google Developers - HTTP Caching
Try It Yourself
Ready to optimize your caching headers?
🔧 Download UXR SEO Analyzer (Free, 100% local analysis)
Disclaimer: The analyzers in this extension are reference guides based on official documentation from MDN, web.dev, and Chrome Developers. They do not represent absolute truths about how search engines evaluate your content—only search engines know their internal algorithms. Use these recommendations as a starting point to improve your site.
Last updated: December 15, 2025