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.
HTTP caching reduces repeated downloads and origin requests, but the policy must match how often content changes and whether it is private. Choose headers by asset type and test what users receive after updates and deployments.
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. Changing the content hash gives updated assets a new URL.
Use this pattern for assets whose URLs change with their contents. 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: the response is immediately stale; addno-cachewhen reuse must require validations-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
A cache may serve a stale response during the configured stale-while-revalidate window while fetching an update. Choose that window based on how much staleness the page can tolerate.
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. Multiple requests may receive the stale response while revalidation is in progress; the allowed staleness is defined by the response directives.
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.
Check whether your deployment process can invalidate the relevant HTML cache entries and verify the result after a release.
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