Setting up immutable cache headers for hashed assets
Modern frontend build pipelines generate content-hashed filenames (e.g., app.8f3a9c.js) to guarantee asset uniqueness across deployments. Despite this, browsers still issue conditional If-None-Match or If-Modified-Since requests on page reloads. These generate unnecessary 304 responses and add measurable network latency.
The immutable directive solves this by explicitly telling the browser that a cached resource will never change during its max-age period. It bypasses revalidation entirely. Before implementing, reviewing the foundational mechanics in HTTP Cache-Control Headers Explained ensures correct directive placement and prevents cache poisoning.
Root Cause Analysis: Why Hashed Assets Trigger 304s Without immutable
Browsers default to a freshness validation strategy during hard reloads or back/forward navigation. Even when a hashed asset exists in the HTTP cache, the browser sends a conditional GET request. It verifies the ETag or Last-Modified timestamp against the origin.
The server responds with a 304 Not Modified. This still consumes TCP handshakes, TLS negotiation, and server CPU cycles. The Cache-Control: immutable directive overrides this behavior. It declares that the resource URL is permanently bound to its content hash. Once cached, the browser serves it directly from disk or memory until max-age expires.
DevTools & Lighthouse Diagnostic Workflow
Execute this diagnostic sequence before and after header deployment to isolate validation overhead.
- Open Chrome DevTools > Network tab. Ensure
Disable cacheis unchecked. - Perform a hard reload (
Ctrl+Shift+R/Cmd+Shift+R). - Filter by
JSorCSS. Identify any hashed files returning304status codes. - Run Lighthouse via the Performance tab. Locate the
Serve static assets with an efficient cache policyaudit. - Flag assets with
max-age < 31536000or missing theimmutabledirective. - Target thresholds:
0%304 responses for hashed assets,>95%Lighthouse cache policy score, and<50msTTFB for cached resources. - If Lighthouse continues to flag assets, verify that intermediate proxies or misconfigured CDN cache keys are not stripping the directive.
Step-by-Step Configuration: Build Tools & Edge Servers
Configuration must target only hashed assets. Applying immutable to unversioned files like index.html or API responses causes permanent stale content delivery.
Webpack: Use webpack-manifest-plugin to map hashes. Inject headers via a custom static file server or Node middleware.
Vite: Configure build.rollupOptions.output.assetFileNames to include hashes. Delegate header assignment to your hosting layer or reverse proxy.
Nginx: Apply a regex location block matching hash patterns.
location ~* \.[0-9a-f]{8,}\.(js|css|png|jpg|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header X-Cache-Status $upstream_cache_status;
}
Cloudflare Edge: Deploy a Transform Rule matching hash patterns.
transforms:
- name: immutable-assets
rule:
target:
url: "*.hash.*"
action:
set_headers:
- name: Cache-Control
value: "public, max-age=31536000, immutable"
operation: set
Express/Node Middleware: Inject headers conditionally during static serving.
app.use('/assets', (req, res, next) => {
if (/\.[0-9a-f]{8,}\.(js|css)$/.test(req.path)) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
}
next();
});
Always verify the exact header string in the raw response before deployment.
Metric Thresholds & Production Validation
Monitor CDN analytics and Real User Monitoring (RUM) data immediately post-deployment. Validate against these strict thresholds:
- CDN cache hit ratio:
≥98%for static assets. - TTFB for cached hashed files:
0-10ms(served directly from browser cache). - Network payload reduction:
>40%on repeat visits. - Core Web Vitals: LCP variance typically drops by
15-30mson 3G/4G networks when validation jitter is eliminated.
Verify header persistence using curl:
curl -I -H 'Cache-Control: max-age=0' https://yourdomain.com/assets/app.[hash].js
The response must return 200 OK with the immutable directive intact. This proves the directive overrides conditional requests even when clients request fresh validation.
Edge Cases & Integration with Service Workers
Service workers intercept fetch requests and can bypass browser cache headers if misconfigured. Ensure your SW fetch handler respects request.cache or explicitly checks caches.match() before falling back to the network.
When combining immutable with stale-while-revalidate, apply immutable only to the hashed payload layer. Do not apply it to the SW cache strategy itself. For complex routing, implement a strict asset manifest validation step in CI/CD to prevent header misapplication.
Understanding how edge nodes propagate these directives across global PoPs is essential for scaling. This aligns with broader architectural patterns covered in Advanced Caching Strategies & CDN Architecture.
Common Mistakes
- Applying
immutableto HTML, JSON, or unversioned API endpoints, causing permanent stale content delivery. - Omitting
max-agealongsideimmutable, which defaults to browser heuristics and breaks the directive. - Using
no-cacheormust-revalidatein the same header string, creating conflicting cache directives. - Failing to update CDN cache keys to ignore query strings, causing cache fragmentation despite correct headers.
- Deploying hashed assets without verifying the hash matches the build output, leading to 404s on cache purge.
FAQ
Does Cache-Control: immutable work if the user clears their browser cache?
No. The directive only applies to resources already stored in the browser's HTTP cache. Once cleared, the browser must fetch the asset from the origin or CDN again. It will respect the directive for subsequent visits.
Can I use immutable alongside stale-while-revalidate?
Technically yes, but it is architecturally redundant. immutable guarantees zero revalidation, while stale-while-revalidate explicitly allows background validation. Use immutable for hashed static assets and stale-while-revalidate for dynamic content.
Why does Lighthouse still flag my hashed assets after adding immutable?
Lighthouse flags assets if max-age is below 31536000, if the header is malformed, or if a service worker intercepts the request and modifies cache behavior. Verify raw response headers via DevTools and ensure your SW fetch handler does not override Cache-Control.
How do I safely purge immutable assets during a deployment rollback? Immutable assets are tied to unique hashes. A rollback simply deploys the previous hash version. Since browsers cache by URL, the old hash remains valid and cached. You do not need to purge immutable assets; revert the build manifest to point to the previous hash.