Browser and HTTP cache layer optimization using cache headers, conditional requests, and validation strategies
Last Updated: 2025-10-25
Activate when:
Prerequisites: caching-fundamentals.md, basic HTTP knowledge
Related Skills: cdn-edge-caching.md, service-worker-caching.md, frontend-performance.md
Browser Request
↓
1. Check browser cache
↓
2. Is cached? Is fresh?
├─ Yes (HIT) → Return from cache
├─ Expired → Revalidate with server
└─ No (MISS) → Request from server
↓
3. Server response with cache headers
↓
4. Store in cache (if cacheable)
↓
5. Return to application
from dataclasses import dataclass
from typing import Optional
from datetime import datetime, timedelta
@dataclass
class HTTPCacheHeaders:
"""HTTP caching headers reference"""
# Modern (HTTP/1.1)
cache_control: str # Primary caching directive
etag: Optional[str] = None # Entity tag for validation
last_modified: Optional[str] = None # Last modification time
# Legacy (HTTP/1.0)
expires: Optional[str] = None # Absolute expiration time
pragma: Optional[str] = None # Legacy no-cache
# Additional
vary: Optional[str] = None # Vary cache by headers
age: Optional[int] = None # Time in cache (seconds)
class CacheDirectives:
"""Cache-Control directive meanings"""
DIRECTIVES = {
# Cacheability
"public": "Cacheable by any cache (browsers, CDNs)",
"private": "Cacheable by browser only, not CDNs",
"no-cache": "Must revalidate before use (NOT 'don't cache')",
"no-store": "Don't cache at all (sensitive data)",
# Expiration
"max-age=<seconds>": "Fresh for N seconds from response time",
"s-maxage=<seconds>": "Shared cache (CDN) max age, overrides max-age",
"stale-while-revalidate=<seconds>": "Serve stale while updating",
"stale-if-error=<seconds>": "Serve stale if origin errors",
# Revalidation
"must-revalidate": "Must not serve stale without revalidation",
"proxy-revalidate": "Shared caches must revalidate",
"immutable": "Never revalidate (content won't change)",
}
class CacheControlExamples:
"""Common Cache-Control configurations"""
@staticmethod
def static_assets():
"""
Static assets with fingerprinting (e.g., app.abc123.js)
Pattern: Long cache with immutable
"""
return {
"header": "Cache-Control",
"value": "public, max-age=31536000, immutable",
"explanation": "Cache for 1 year, never revalidate",
"use_for": ["JS/CSS with hash", "Versioned images"]
}
@staticmethod
def html_pages():
"""
HTML pages (dynamic content)
Pattern: No caching or short TTL with revalidation
"""
return {
"header": "Cache-Control",
"value": "public, max-age=0, must-revalidate",
"explanation": "Always revalidate before serving",
"use_for": ["HTML pages", "API HTML responses"]
}
@staticmethod
def api_responses():
"""
API responses (read-heavy endpoints)
Pattern: Short TTL with private caching
"""
return {
"header": "Cache-Control",
"value": "private, max-age=300",
"explanation": "Cache in browser for 5 minutes",
"use_for": ["User-specific API data", "Dashboard APIs"]
}
@staticmethod
def cdn_assets():
"""
CDN-delivered assets
Pattern: Long browser cache, longer CDN cache
"""
return {
"header": "Cache-Control",
"value": "public, max-age=3600, s-maxage=86400",
"explanation": "Browser: 1 hour, CDN: 24 hours",
"use_for": ["Images", "Fonts", "Public assets"]
}
@staticmethod
def no_caching():
"""
Sensitive or always-fresh data
Pattern: Complete cache prevention
"""
return {
"header": "Cache-Control",
"value": "no-store, no-cache, must-revalidate, private",
"explanation": "Never cache, always fetch fresh",
"use_for": ["Sensitive data", "Real-time data"]
}
@staticmethod
def stale_while_revalidate():
"""
Serve stale content while updating (2024 pattern)
Pattern: Balance freshness and performance
"""
return {
"header": "Cache-Control",
"value": "public, max-age=60, stale-while-revalidate=300",
"explanation": "Serve stale for 5min while revalidating",
"use_for": ["News feeds", "Social media timelines"]
}
# Implementation examples
def set_cache_headers_flask():
"""Flask example"""
from flask import make_response
@app.route('/api/data')
def api_data():
response = make_response({"data": "value"})
response.headers['Cache-Control'] = 'private, max-age=300'
return response
def set_cache_headers_express():
"""Express.js example"""
# JavaScript
"""
app.get('/api/data', (req, res) => {
res.set('Cache-Control', 'private, max-age=300');
res.json({ data: 'value' });
});
"""
def set_cache_headers_fastapi():
"""FastAPI example"""
from fastapi import Response
@app.get("/api/data")
async def api_data(response: Response):
response.headers["Cache-Control"] = "private, max-age=300"
return {"data": "value"}
class ModernCacheDirectives:
"""2024 caching best practices"""
@staticmethod
def immutable_pattern():
"""
Immutable directive (Chrome 54+, Firefox 49+)
Prevents revalidation even on reload
"""
return {
"header": "Cache-Control: public, max-age=31536000, immutable",
"benefit": "No revalidation on refresh (F5)",
"use_case": "Fingerprinted assets (app.[hash].js)",
"support": "All modern browsers (2024)"
}
@staticmethod
def stale_while_revalidate_pattern():
"""
Serve stale while updating in background
Chrome 75+, Firefox 68+
"""
return {
"header": "Cache-Control: max-age=60, stale-while-revalidate=86400",
"behavior": [
"< 60s: Serve from cache (fresh)",
"60s - 24h: Serve stale, revalidate async",
"> 24h: Block until revalidated"
],
"benefit": "Instant responses + background updates",
"support": "Chrome, Firefox, Safari 16+"
}
@staticmethod
def stale_if_error_pattern():
"""
Serve stale on server errors
Resilience pattern for high availability
"""
return {
"header": "Cache-Control: max-age=3600, stale-if-error=86400",
"behavior": "Serve stale up to 24h if origin returns 5xx",
"benefit": "Graceful degradation during outages",
"use_case": "Critical APIs, high-availability content"
}
Concept: Hash/version of content for validation
import hashlib
from typing import Optional
class ETagHandler:
"""ETag generation and validation"""
@staticmethod
def generate_etag(content: str) -> str:
"""
Generate ETag from content
Strong ETag: byte-for-byte match required
Weak ETag: semantic equivalence (W/"...")
"""
# Strong ETag
hash_value = hashlib.sha256(content.encode()).hexdigest()[:16]
return f'"{hash_value}"'
@staticmethod
def generate_weak_etag(content: str) -> str:
"""
Weak ETag for semantic equivalence
Use when minor differences acceptable (whitespace, formatting)
"""
hash_value = hashlib.sha256(content.encode()).hexdigest()[:16]
return f'W/"{hash_value}"'
@staticmethod
def validate_etag(request_etag: str, current_etag: str) -> bool:
"""Check if ETags match"""
# Strip W/ prefix for weak comparison
req = request_etag.replace('W/', '')
cur = current_etag.replace('W/', '')
return req == cur
# Flask implementation
def etag_example_flask():
from flask import Flask, request, make_response
app = Flask(__name__)
@app.route('/api/data')
def api_data():
content = get_current_data() # {"data": "value"}
content_str = json.dumps(content, sort_keys=True)
# Generate ETag
etag = ETagHandler.generate_etag(content_str)
# Check If-None-Match header
if_none_match = request.headers.get('If-None-Match')
if if_none_match == etag:
# Content unchanged - return 304 Not Modified
response = make_response('', 304)
response.headers['ETag'] = etag
return response
# Content changed - return 200 with data
response = make_response(content)
response.headers['ETag'] = etag
response.headers['Cache-Control'] = 'private, max-age=300'
return response
# FastAPI implementation
from fastapi import FastAPI, Request, Response, status
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/api/data")
async def api_data(request: Request):
content = {"data": "value"}
content_str = json.dumps(content, sort_keys=True)
# Generate ETag
etag = ETagHandler.generate_etag(content_str)
# Check If-None-Match
if_none_match = request.headers.get("if-none-match")
if if_none_match == etag:
# Not modified
return Response(status_code=status.HTTP_304_NOT_MODIFIED, headers={"ETag": etag})
# Return data with ETag
return JSONResponse(
content=content,
headers={
"ETag": etag,
"Cache-Control": "private, max-age=300"
}
)
from datetime import datetime
from email.utils import formatdate, parsedate_to_datetime
class LastModifiedHandler:
"""Last-Modified header handling"""
@staticmethod
def format_http_date(dt: datetime) -> str:
"""Convert datetime to HTTP date format"""
# HTTP date: Wed, 21 Oct 2015 07:28:00 GMT
return formatdate(dt.timestamp(), usegmt=True)
@staticmethod
def parse_http_date(date_str: str) -> datetime:
"""Parse HTTP date string to datetime"""
return parsedate_to_datetime(date_str)
@staticmethod
def is_modified_since(last_modified: datetime, if_modified_since: Optional[str]) -> bool:
"""
Check if content was modified since given time
Returns: True if modified (send full response)
"""
if not if_modified_since:
return True # No conditional request
try:
client_time = LastModifiedHandler.parse_http_date(if_modified_since)
# Compare (truncate to seconds, HTTP dates have 1s precision)
return last_modified.timestamp() > client_time.timestamp()
except Exception:
return True # Parse error, send full response
# Flask example
@app.route('/api/document/<int:doc_id>')
def get_document(doc_id):
doc = get_document_from_db(doc_id) # Returns {content, updated_at}
last_modified = doc['updated_at']
last_modified_str = LastModifiedHandler.format_http_date(last_modified)
# Check If-Modified-Since
if_modified_since = request.headers.get('If-Modified-Since')
if not LastModifiedHandler.is_modified_since(last_modified, if_modified_since):
# Not modified
response = make_response('', 304)
response.headers['Last-Modified'] = last_modified_str
return response
# Modified - return full document
response = make_response(doc['content'])
response.headers['Last-Modified'] = last_modified_str
response.headers['Cache-Control'] = 'private, max-age=300'
return response
Purpose: Vary cache by request headers (e.g., Accept-Encoding, User-Agent)
class VaryHeaderExamples:
"""Vary header patterns"""
@staticmethod
def vary_by_encoding():
"""
Vary by Accept-Encoding
Cache separate versions for gzip, br, etc.
"""
return {
"header": "Vary: Accept-Encoding",
"use_case": "Compressed responses",
"example": "Same content, different compression"
}
@staticmethod
def vary_by_auth():
"""
Vary by Authorization
Prevent caching of authenticated content
"""
return {
"header": "Vary: Authorization",
"use_case": "User-specific content",
"warning": "Often better to use Cache-Control: private"
}
@staticmethod
def vary_by_multiple():
"""
Vary by multiple headers
Cache multiplied by combinations
"""
return {
"header": "Vary: Accept-Encoding, Accept-Language",
"use_case": "i18n content with compression",
"warning": "Cache fragmentation (many variants)"
}
# Implementation
@app.route('/api/content')
def get_content():
accept_encoding = request.headers.get('Accept-Encoding', '')
accept_language = request.headers.get('Accept-Language', 'en')
# Generate appropriate content
content = get_localized_content(accept_language)
response = make_response(content)
response.headers['Vary'] = 'Accept-Encoding, Accept-Language'
response.headers['Cache-Control'] = 'public, max-age=3600'
return response
Best Practice: Include content hash in filename
import hashlib
import os
class CacheBusting:
"""Cache busting strategies"""
@staticmethod
def fingerprint_filename(filepath: str) -> str:
"""
Add content hash to filename
Example: app.js → app.a1b2c3d4.js
"""
with open(filepath, 'rb') as f:
content = f.read()
# Generate hash
hash_value = hashlib.md5(content).hexdigest()[:8]
# Insert hash before extension
name, ext = os.path.splitext(filepath)
return f"{name}.{hash_value}{ext}"
@staticmethod
def query_string_versioning(url: str, version: str) -> str:
"""
Add version query parameter
Less ideal (some caches ignore query params)
"""
separator = '&' if '?' in url else '?'
return f"{url}{separator}v={version}"
# Build tool integration (example)
"""
# Webpack/Vite automatically handles this:
// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash].js',
},
};
// Results in: main.a1b2c3d4.js
// HTML references updated automatically
"""
# HTML generation
def generate_asset_url(asset_path: str, version: Optional[str] = None) -> str:
"""
Generate versioned asset URL
Development: /static/app.js
Production: /static/app.a1b2c3d4.js
"""
if os.environ.get('ENV') == 'production':
return CacheBusting.fingerprint_filename(asset_path)
else:
return asset_path
<!-- Immutable pattern with long cache -->
<link rel="stylesheet" href="/static/app.a1b2c3d4.css">
<!-- Server sends: Cache-Control: public, max-age=31536000, immutable -->
<!-- Non-fingerprinted assets: short cache -->
<link rel="stylesheet" href="/static/app.css">
<!-- Server sends: Cache-Control: public, max-age=300 -->
# Check response headers
curl -I https://example.com/api/data
# Check with If-None-Match (ETag validation)
curl -H "If-None-Match: \"abc123\"" -I https://example.com/api/data
# Check with If-Modified-Since
curl -H "If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT" -I \
https://example.com/api/data
# Verbose output
curl -v https://example.com/api/data
// Chrome DevTools Network tab:
// 1. Open DevTools (F12)
// 2. Network tab
// 3. Disable cache checkbox (for testing)
// 4. Look at "Size" column:
// - "(disk cache)" = from disk cache
// - "(memory cache)" = from memory cache
// - "304 Not Modified" = conditional request successful
// - File size = fresh request
// Inspect headers
fetch('/api/data')
.then(response => {
console.log('Cache-Control:', response.headers.get('cache-control'));
console.log('ETag:', response.headers.get('etag'));
console.log('Age:', response.headers.get('age'));
});
class StaticAssetCaching:
"""Optimal caching for static assets"""
@staticmethod
def configure_asset_caching():
"""
Different strategies for different asset types
"""
return {
"fingerprinted_assets": {
"pattern": "*.{hash}.{js,css,png,jpg}",
"cache_control": "public, max-age=31536000, immutable",
"explanation": "Content-addressed, never changes"
},
"fonts": {
"pattern": "*.{woff,woff2,ttf}",
"cache_control": "public, max-age=31536000, immutable",
"explanation": "Fonts rarely change"
},
"images_without_hash": {
"pattern": "*.{png,jpg,svg}",
"cache_control": "public, max-age=2592000", # 30 days
"explanation": "Long but not immutable"
},
"html": {
"pattern": "*.html",
"cache_control": "public, max-age=0, must-revalidate",
"explanation": "Always check for updates"
}
}
# Nginx configuration
"""
# Fingerprinted assets
location ~* \.[\da-f]{8}\.(js|css)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Fonts
location ~* \.(woff|woff2|ttf)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# HTML
location ~* \.html$ {
add_header Cache-Control "public, max-age=0, must-revalidate";
}
"""
# WRONG: Missing cache headers
@app.route('/api/data')
def api_data():
return {"data": "value"}
# Browser uses heuristic caching (unpredictable)
# CORRECT: Explicit cache headers
@app.route('/api/data')
def api_data():
response = make_response({"data": "value"})
response.headers['Cache-Control'] = 'private, max-age=300'
return response
# WRONG: Using no-cache to prevent caching
response.headers['Cache-Control'] = 'no-cache'
# "no-cache" means "revalidate before use", NOT "don't cache"
# CORRECT: Use no-store to prevent caching
response.headers['Cache-Control'] = 'no-store'
# WRONG: Public cache for user-specific data
@app.route('/api/user/profile')
def user_profile():
response = make_response(get_user_data())
response.headers['Cache-Control'] = 'public, max-age=3600'
# CDN will cache and serve to wrong users!
return response
# CORRECT: Private cache only
@app.route('/api/user/profile')
def user_profile():
response = make_response(get_user_data())
response.headers['Cache-Control'] = 'private, max-age=300'
return response
| Directive | Meaning | Use Case | |-----------|---------|----------| | public | Any cache can store | Static assets, public APIs | | private | Browser only, no CDN | User-specific data | | no-cache | Revalidate before use | HTML, frequently updated | | no-store | Don't cache | Sensitive data | | max-age=N | Fresh for N seconds | All cacheable content | | s-maxage=N | CDN max age | CDN-delivered content | | immutable | Never revalidate | Fingerprinted assets | | stale-while-revalidate=N | Serve stale while updating | Balance freshness/performance |
| Code | Meaning | Use Case | |------|---------|----------| | 200 OK | Full response | Normal response | | 304 Not Modified | Content unchanged | Successful conditional request | | 412 Precondition Failed | Condition not met | Failed conditional request |
Next Steps:
cdn-edge-caching.md → CDN configuration and optimizationservice-worker-caching.md → Progressive Web App cachingcache-performance-monitoring.md → Measuring cache effectivenessFoundations:
caching-fundamentals.md → Core caching conceptsfrontend-performance.md → Overall web performanceHTTP caching optimizes web performance through browser and intermediate caches:
Key takeaways:
Next: Move to cdn-edge-caching.md for CDN optimization.