HTTP Caching Strategies for Web Performance: A Developer's Reference
Understand HTTP caching headers, Cache-Control directives, CDN caching, and stale-while-revalidate for faster TTFB and better Core Web Vitals without sacrificing freshness.
Your application is deployed. Your CDN is in front of it. But your Time to First Byte is still 800ms and your LCP is failing. You add aggressive caching — and suddenly your users see stale content for days after you deploy. Caching is the part of web performance where the tradeoffs are sharpest and the misconfiguration costs are highest. This guide covers the practical HTTP caching strategies that improve performance without breaking your release process.
Why HTTP Caching Directly Affects Core Web Vitals
Largest Contentful Paint has four phases, and Time to First Byte (TTFB) is the first. If TTFB is high, LCP cannot be low, regardless of how well your images are optimised. The Google recommendation is TTFB under 600ms at the 75th percentile, with a target of under 200ms.
HTTP caching reduces TTFB in two ways:
- Browser cache: On repeat visits, the browser serves the resource from local disk without any network request at all. TTFB is effectively zero.
- CDN cache: On first visits or after cache expiry, the CDN serves the cached response from an edge node near the user instead of forwarding the request to your origin server. TTFB drops from potentially seconds to tens of milliseconds.
Both require correct Cache-Control headers.
The Core Caching Headers
Cache-Control
Cache-Control is the primary header for controlling caching behaviour. It can appear in both request and response headers, but the response header is what you control as a server developer.
Key directives:
max-age=N: The response can be cached for N seconds. After that, the cache must revalidate with the origin before serving it again.s-maxage=N: Same as max-age, but applies only to shared caches (CDNs). Overrides max-age for CDNs while letting browsers use max-age. Useful when you want different TTLs for browser vs. CDN.no-cache: The response can be cached, but the cache must validate it with the origin on every request. Does not mean "do not cache" — it means "always revalidate."no-store: Do not cache the response at all. Use for genuinely sensitive data.public: The response can be cached by any cache including shared CDN caches, even if the request was made with authentication headers.private: The response should only be cached by the browser, not by shared CDN caches. Use for personalised responses.immutable: Signals to the browser that the resource will not change during its max-age window. Prevents conditional revalidation requests on page reload.
ETag and Last-Modified
When a cached response expires, the browser or CDN sends a conditional request to the origin to check if the resource has changed. The server uses ETag (an opaque version identifier) or Last-Modified (a timestamp) to enable this without re-sending the full response body.
If unchanged: server responds with 304 Not Modified and no body. The cache keeps the response and resets the TTL. If changed: server sends 200 OK with the new body.
Conditional requests reduce bandwidth on cache misses while still allowing the origin to serve fresh content.
HTTP Caching Strategies by Asset Type
Immutable Static Assets (JS, CSS, Images with Content Hashes)
Modern build tools (webpack, Vite, esbuild) generate filenames with content hashes: main.a3f8c92.js. When the file changes, the hash changes, and the URL changes. Old cached copies are never served for new content because the URL is new.
This makes it safe to cache these assets forever:
Cache-Control: public, max-age=31536000, immutable
One year TTL. immutable tells browsers not to revalidate even on hard reload. Maximum performance, zero cache invalidation problem.
This is the single highest-impact caching decision you can make. Most application JavaScript and CSS should be served this way.
HTML Documents
HTML is the entry point. It contains URLs to your versioned assets. Cache it too aggressively and users see stale pages. Do not cache it at all and every visit hits your origin.
A practical approach: short CDN cache with stale-while-revalidate.
Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=86400
max-age=0: browsers always revalidate (via conditional request)s-maxage=60: CDN caches the response for 60 secondsstale-while-revalidate=86400: CDN serves the stale cached response immediately while fetching a fresh copy in the background, for up to 24 hours after the s-maxage expires
This gives you fast TTFB from CDN for most requests, background freshness updates, and no staleness problem for users — they get the stale version for at most the length of one revalidation cycle.
API Responses
Public API data that is the same for all users: use s-maxage and stale-while-revalidate with a TTL appropriate to how often the data changes.
Personalised API data: use private, no-store to prevent CDN caching. This data must always come from your origin.
For authenticated API responses that are user-specific but safe to cache briefly: private, max-age=30 allows browser caching for 30 seconds, avoiding repeat network requests during a session, without risking CDN sharing of private data.
Images
Images without content-hash filenames (uploaded images, CMS-managed assets) deserve a moderate TTL:
Cache-Control: public, max-age=604800, stale-while-revalidate=86400
One week max-age with a one-day stale-while-revalidate window. Long enough to benefit repeat visitors, short enough that changes propagate within a week. For images where changes are more frequent, reduce max-age accordingly.
stale-while-revalidate: The Performance-Freshness Sweet Spot
stale-while-revalidate is one of the most useful caching directives and is underused. It tells a cache: "if the response is stale by less than N seconds, serve the stale copy immediately and fetch a fresh one in the background."
The user gets a fast response with zero wait. The cache gets updated for the next request. The only tradeoff is that one request per cache hit cycle sees content that is at most one TTL cycle old.
For most web applications, this is an excellent tradeoff. Users care about fast pages. Seeing content that is 60 seconds old on a content site is inconsequential. Combine it with a reasonable s-maxage and you have near-instant TTFB from CDN with background freshness.
Cache Invalidation on Deployment
The practical challenge: you ship a bug fix and users are still seeing the old page from CDN cache.
Strategies:
- Purge API: most CDNs expose an API to purge specific URLs or tags on deployment. Wire your deployment pipeline to purge HTML pages (your versioned assets do not need purging — the URL changes).
- Surrogate keys / cache tags: tag cached responses at the CDN level. Purge all responses tagged
article-123when that article is updated. - Versioned HTML paths: uncommon but effective — include a build version in your HTML URL so the CDN always sees it as a new resource.
For most teams, a deployment-triggered CDN purge of the site's HTML pages is sufficient and takes minutes to implement.
Infrastructure Checklist
- Immutable static assets (JS, CSS, hashed images) have
max-age=31536000, immutable - HTML documents have short s-maxage with
stale-while-revalidate - CDN purge runs on every deployment for HTML resources
- ETags or Last-Modified headers present on all cacheable responses
- Personalised and sensitive responses use
privateorno-store - TTFB measured at p75 in production and below 600ms
Caching is infrastructure-level performance work. Done correctly, it is one of the highest-ROI investments you can make — better TTFB, lower origin load, and better Core Web Vitals, often without touching a single line of application code.
If your team needs help designing a caching and CDN strategy for a high-traffic application, the Clixo engineering team is available for a technical consultation.