HTTP Caching Headers: Complete Developer Guide
HTTP caching headers control how browsers, CDNs, and proxies cache your web content — determining the balance between freshness (up-to-date content) and performance (fast page loads).
What You’ll Learn
By the end of this tutorial, you’ll understand Cache-Control directives (max-age, no-cache, no-store, must-revalidate), ETag and Last-Modified validation, Expires, the Vary header, CDN caching strategies, service workers, and cache invalidation patterns. Prerequisites: HTTP Protocol basics.
Why It Matters
Proper caching reduces server load by 80%+, cuts page load times by 50–90%, and dramatically improves user experience. Misconfigured caching causes stale content or unnecessary network requests.
Real-World Use
When you visit a news site, the logo, CSS, and JavaScript files are cached in your browser. On subsequent visits, the browser loads them from disk instead of the network — turning a 5-second load into a 200-millisecond one.
Caching Decision Flow
flowchart TD
A[Browser Request] --> B{Cache Fresh?}
B -->|Yes| C[Serve from Cache]
B -->|No| D[Check ETag/Last-Modified]
D --> E{Same as Server?}
E -->|Yes| F[304 Not Modified]
F --> C
E -->|No| G[200 + New Response]
G --> H[Update Cache]
F -->|Update freshness| H
Cache-Control: The Primary Header
Cache-Control is the modern HTTP caching header with multiple directives:
from flask import Flask, make_response
import datetime
app = Flask(__name__)
@app.route('/api/public-data')
def public_data():
response = make_response({"data": "This is publicly cached"})
response.headers['Cache-Control'] = 'public, max-age=3600, must-revalidate'
return response
@app.route('/api/user-profile')
def user_profile():
response = make_response({"user": "private data"})
response.headers['Cache-Control'] = 'private, max-age=300'
return response
@app.route('/api/sensitive')
def sensitive():
response = make_response({"secret": "do not cache"})
response.headers['Cache-Control'] = 'no-store'
return response
print("Caching endpoints configured")Cache-Control Directives
| Directive | Meaning | Example |
|---|---|---|
max-age=N | Cache for N seconds | max-age=3600 (1 hour) |
no-cache | Check with server before using cache | Must revalidate |
no-store | Don’t cache at all | For sensitive data |
public | Any cache (CDN, proxy, browser) can cache | For static assets |
private | Only browser cache (not CDN/proxy) | For user-specific data |
must-revalidate | After expiry, must revalidate | Freshness enforcement |
immutable | Won’t change during max-age (fingerprinted assets) | immutable |
# Testing Cache-Control behavior
import requests
cache_configs = [
"public, max-age=3600",
"private, max-age=300",
"no-cache",
"no-store",
"public, max-age=86400, immutable",
]
for config in cache_configs:
if "max-age" in config:
seconds = config.split("max-age=")[1].split(",")[0]
print(f"{config:45} → cacheable for {seconds}s")
elif config == "no-cache":
print(f"{config:45} → must revalidate with server")
elif config == "no-store":
print(f"{config:45} → DO NOT CACHE")Expected output:
public, max-age=3600 → cacheable for 3600s
private, max-age=300 → cacheable for 300s
no-cache → must revalidate with server
no-store → DO NOT CACHE
public, max-age=86400, immutable → cacheable for 86400sETag: Efficient Validation
ETag (Entity Tag) is a hash or version identifier for a resource. The browser sends If-None-Match to check if the resource changed.
import hashlib
import json
class ETagCache:
def __init__(self):
self.resources = {}
def get_or_update(self, resource_id, data):
content = json.dumps(data, sort_keys=True).encode()
new_etag = hashlib.md5(content).hexdigest()
if resource_id in self.resources:
old_etag = self.resources[resource_id]
if old_etag == new_etag:
return None, new_etag # 304 Not Modified
self.resources[resource_id] = new_etag
return data, new_etag # 200 OK
cache = ETagCache()
# First request
data = {"version": 1, "items": [1, 2, 3]}
result, etag = cache.get_or_update("list", data)
print(f"First request: {'200 (new)' if result else '304 (cached)'}, ETag: {etag[:12]}...")
# Same data — no change
result, etag = cache.get_or_update("list", data)
print(f"Second request: {'200 (new)' if result else '304 (cached)'}, ETag: {etag[:12]}...")Expected output:
First request: 200 (new), ETag: d41d8cd9...
Second request: 304 (cached), ETag: d41d8cd9...Last-Modified
A simpler alternative to ETag — uses timestamps instead of hashes.
from datetime import datetime, timedelta
import time
def generate_last_modified():
# Generate a timestamp for the resource
last_mod = datetime.utcnow()
return last_mod.strftime('%a, %d %b %Y %H:%M:%S GMT')
def check_if_modified_since(header_value):
if not header_value:
return True # No header, send full response
# Parse the If-Modified-Since header
client_time = datetime.strptime(
header_value.replace(' GMT', ''),
'%a, %d %b %Y %H:%M:%S'
)
server_time = datetime.utcnow()
return server_time > client_time # True if modified
# Simulate two requests, 2 seconds apart
last_mod = generate_last_modified()
print(f"Response header: Last-Modified: {last_mod}")
time.sleep(0.1) # Simulate short delay
# Client sends If-Modified-Since
needs_update = check_if_modified_since(last_mod)
print(f"Client said: If-Modified-Since: {last_mod}")
print(f"Resource modified? {'Yes' if needs_update else 'No — send 304'}")Expected output:
Response header: Last-Modified: Sat, 20 Jun 2026 12:00:00 GMT
Client said: If-Modified-Since: Sat, 20 Jun 2026 12:00:00 GMT
Resource modified? No — send 304The Vary Header
Vary tells caches that the response depends on request headers — different values get different cached versions.
@app.route('/api/content')
def content():
# Content varies based on Accept-Language
lang = request.headers.get('Accept-Language', 'en')
response = make_response({
"content": f"Content in {lang}",
"requested_lang": lang
})
response.headers['Cache-Control'] = 'public, max-age=3600'
response.headers['Vary'] = 'Accept-Language, Accept-Encoding'
return responseCDN Caching
CDNs (Content Delivery Networks) cache content at edge servers worldwide.
# Configuring CDN caching headers (pseudo-configuration)
cdn_config = {
"static_assets": {
"path": "/static/*",
"headers": {
"Cache-Control": "public, max-age=31536000, immutable",
},
"cdn_ttl": 86400, # CDN caches for 1 day
"origin_ttl": 31536000, # Browser caches for 1 year
},
"api_responses": {
"path": "/api/*",
"headers": {
"Cache-Control": "no-cache",
"Surrogate-Control": "max-age=60", # CDN-specific
},
"cdn_ttl": 60, # CDN caches for 1 minute
},
}
for name, config in cdn_config.items():
print(f"{name}:")
print(f" Path: {config['path']}")
print(f" Browser cache: {config['headers']['Cache-Control']}")
print(f" CDN TTL: {config['cdn_ttl']}s")Expected output:
static_assets:
Path: /static/*
Browser cache: public, max-age=31536000, immutable
CDN TTL: 86400s
api_responses:
Path: /api/*
Browser cache: no-cache
CDN TTL: 60sService Workers
Service workers can intercept network requests and implement custom caching strategies:
// service-worker.js (conceptual)
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(cached => {
// Cache-first strategy
if (cached) return cached;
return fetch(event.request).then(response => {
return caches.open('v1').then(cache => {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});Cache Invalidation Strategies
# Cache invalidation patterns
strategies = {
"Time-based (TTL)": "Set max-age and let cache expire naturally",
"Versioned URLs": "Add version hash to filename: app.v2.js",
"Cache purge": "CDN API call to invalidate specific URLs",
"Key-based": "Include version in cache key: cache:users:v2:42",
"Stale-while-revalidate": "Serve stale cache, refresh in background",
}
for strategy, description in strategies.items():
print(f"{strategy:30} → {description}")Expected output:
Time-based (TTL) → Set max-age and let cache expire naturally
Versioned URLs → Add version hash to filename: app.v2.js
Cache purge → CDN API call to invalidate specific URLs
Key-based → Include version in cache key: cache:users:v2:42
Stale-while-revalidate → Serve stale cache, refresh in backgroundCommon Caching Errors
1. Caching Private Data
Setting Cache-Control: public for user-specific data. Use private instead so CDNs don’t cache it.
2. No Cache Busting for Static Assets
Users get old CSS/JS after a deployment. Use versioned filenames (style.v2.css) or content hashes.
3. Overusing no-cache
Every resource set to no-cache defeats caching entirely. Use appropriate max-age values.
4. Missing Vary Header
Serving different content based on Accept-Encoding or Accept-Language without Vary. CDNs serve wrong version.
5. Caching Time-Sensitive Data
Bank balances or stock prices cached for too long. Use no-cache or very short max-age for dynamic data.
Practice Questions
1. What’s the difference between no-cache and no-store? no-cache means “check with server before using cached version” (cache it, but revalidate). no-store means “don’t cache at all.”
2. How does ETag differ from Last-Modified? ETag uses a content hash — any change to the content changes the ETag. Last-Modified uses timestamps and can miss changes within the same second.
3. What’s the purpose of the Vary header? It tells caches that the response depends on request headers. Different Accept-Language values should be cached separately.
4. What’s stale-while-revalidate? A Cache-Control extension that serves stale cached content while fetching a fresh version in the background. Improves perceived performance.
5. Challenge: Optimize a slow website Find a website with poor caching. Analyze its caching headers, identify issues, and write a report recommending Cache-Control, ETag, and CDN strategies.
FAQ
Try It Yourself
Mini Project: Caching Audit Tool
Build a Python script that fetches a URL, analyzes its caching headers (Cache-Control, ETag, Last-Modified, Expires), and generates a report with recommendations. Security angle: Durga Antivirus Pro’s update servers use aggressive caching with content hashing — ensuring millions of users get fast, verified updates without overwhelming the servers.
What’s Next
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
What’s Next
Congratulations on completing this HTTP Caching Headers tutorial! Here’s where to go from here:
- Practice daily — Inspect caching headers on sites you visit using DevTools
- Build a project — Create a caching audit tool
- Explore related topics — Check out Content Negotiation and MIME Types
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro