The HTTP Caching Strategy That Most Teams Get Wrong
HTTP caching is the mechanism by which browsers and CDNs store responses and serve them without contacting the origin server. The correct caching strategy depends on whether the content changes frequently, whether it is user-specific, and what the acceptable staleness window is. Most teams either over-cache (serving stale user-specific data) or under-cache (disabling caching for static assets that could safely be cached for months). The difference between a correct and incorrect caching strategy is measurable in page load time, infrastructure costs, and the correctness of what users see.
Written by Yashveer Singh, founder of Yashveer Labs.
What you actually need to know
- Static assets (JavaScript, CSS, images) with content-based hashes in their filenames should be cached for one year (
max-age=31536000, immutable). They will not be stale because the URL changes when the content changes. - HTML pages should use
no-cacheor very shortmax-age(60-300 seconds) withmust-revalidate. They reference the hashed static assets; if the HTML is stale, users see the wrong asset URLs. - API responses with user-specific data must use
Cache-Control: privateto prevent CDN caching. CDN caching of user-specific data is a privacy breach. stale-while-revalidateis the right strategy for public content that changes infrequently -- instant response plus fresh content on the next request.- Incorrect cache headers that cannot be changed without a full cache purge are more expensive to fix than misconfigured code. Test caching behavior in staging before deploying.
| Content Type | Recommended Cache-Control | CDN Cacheable? | Staleness Risk |
|---|---|---|---|
| Hashed JS/CSS/fonts | max-age=31536000, immutable | Yes | None (hash changes with content) |
| Non-hashed images | max-age=86400 | Yes | Low (images change infrequently) |
| HTML pages | no-cache or max-age=60 | Maybe (short TTL) | Medium |
| User-specific API responses | private, max-age=60 | No | Low (private to browser) |
| Public API responses | public, max-age=300, stale-while-revalidate=600 | Yes | Low to medium |
| Authentication state | no-store | No | Must not be cached |
The core argument
HTTP caching is one of the highest-return performance optimizations available -- correctly configured, it serves responses directly from the browser cache (zero network latency) or from the CDN (low latency, no origin server load). Incorrectly configured, it either serves stale content (users see the wrong version of the product) or disables caching for content that could safely be cached (unnecessary origin server load and worse performance for users).
The mistakes in both directions are common. Teams that disable caching for everything ("we do not want to think about it") pay the performance cost of unnecessary origin requests for content that does not change per-user. Teams that cache everything with aggressive TTLs serve stale HTML pages that reference old JavaScript bundles, creating version mismatch errors that appear as blank pages or broken functionality.
The correct strategy is not complicated -- it is a set of rules that apply to different content types, applied consistently. The complication is that incorrect caching is hard to test locally (local development often disables caching) and hard to debug in production (the cached version and the current version can be different in ways that are only visible to specific users with specific browser caches).
The static asset caching strategy
The canonical strategy for static assets (JavaScript bundles, CSS files, fonts, images) that are served by a web application:
Use content-based hashing in the file name. Most modern build systems (Vite, Webpack, Next.js) do this automatically: app.js becomes app.abc123def.js where abc123def is derived from the file's content. When the file changes, the hash changes and the URL changes.
Because the URL changes when the content changes, the asset can be cached forever -- any URL that was previously valid is still valid, and any URL that is no longer valid (because the content changed) is now a different URL that will be fetched fresh. The cache headers for these files:
`` Cache-Control: public, max-age=31536000, immutable ``
max-age=31536000 is one year. immutable is a hint to the browser that the resource will not change while it is valid -- the browser can skip the conditional revalidation that it would otherwise attempt for long-TTL resources.
The one exception: fonts served from Google Fonts or similar CDNs often do not have hashed URLs and should be cached for a shorter period (30 days is typical).
The HTML page caching strategy
HTML pages reference the static assets by URL. If the HTML page is cached aggressively and an asset URL changes, the cached HTML points to an old asset URL that may no longer exist. This produces the "blank page" failure mode where the browser loads the cached HTML but cannot find the referenced JavaScript file.
The correct HTML caching strategy: no-cache with must-revalidate, or a very short max-age (60-300 seconds).
no-cache means the browser must check with the server before using the cached version. The server responds with 304 Not Modified if the content has not changed (no body, low bandwidth) or with the new content if it has changed. This ensures users always get the current HTML while the server can still avoid sending the full body when the content has not changed.
For Next.js and similar frameworks that generate HTML at build time or request time, the default behavior handles this correctly -- Next.js sets appropriate cache headers for each page type based on whether the page is static, server-rendered, or client-side rendered.
The API response caching strategy
API responses require case-by-case analysis. The two categories that matter:
User-specific responses: Any API response that returns data specific to the authenticated user (profile data, private content, account state, permissions) must use Cache-Control: private to prevent CDN caching. The browser can still cache these responses (the user's browser sees only their own data), but the CDN must not cache them (the CDN serves responses to all users, not just the owner of the data). Failing to use private on user-specific API responses can result in user A's data being served to user B -- a privacy breach that is difficult to detect and potentially severe.
Public responses: API responses for public content (blog posts, product listings, pricing information) can use Cache-Control: public, max-age=N with a CDN. The TTL should match the content's update frequency -- blog posts that are updated occasionally can have a 5-minute TTL; prices that change frequently should have a shorter TTL or use no-cache with stale-while-revalidate.
The stale-while-revalidate pattern
stale-while-revalidate is a directive that allows serving a stale cached response while fetching a fresh one in the background:
`` Cache-Control: public, max-age=60, stale-while-revalidate=300 ``
This means: serve the cached response immediately if it was cached within the last 60 seconds (fresh). If the cached response is between 60 and 300 seconds old (stale but within the stale-while-revalidate window), serve it immediately and fetch a fresh version in the background. If the cached response is more than 300 seconds old, fetch a fresh version before serving.
The benefit: the user never waits for a revalidation request. The cost: the user may see slightly stale data for the duration of the background fetch. This tradeoff is acceptable for content that changes infrequently and where seeing data that is a few minutes old has no meaningful consequence.
Common mistakes teams make with HTTP caching
- Not using content hashing for static assets. Deploying
app.jswithout a hash and setting a long TTL means users see old JavaScript for the duration of the TTL after a deployment. - Using
no-cachefor static assets that should be cached forever.no-cachemeans "check with the server before using the cache" -- it requires a network round trip on every request. Hashed static assets should never need this. - Not setting
privateon user-specific API responses. CDN caching of user-specific data is a privacy breach. - Setting
max-age=0instead ofno-cache.max-age=0sets the TTL to zero but does not require revalidation.no-cacherequires revalidation before serving cached content. They are not equivalent. - Not testing caching behavior with cache cleared. Browser developer tools allow disabling the cache; clearing the cache simulates a first visit. Testing after clearing the cache reveals whether the caching strategy is working as expected.
Where to start: a 3-step caching audit
Step 1: Check the Cache-Control headers on your three most-visited page types. In Chrome DevTools, open the Network tab, load the page with cache disabled, and examine the response headers for the HTML and the main JavaScript bundle. Verify that the HTML has no-cache or a short max-age, and that the JavaScript bundle has content hashing in its filename and max-age=31536000.
Step 2: Check the Cache-Control headers on your three most-called API endpoints. Verify that user-specific endpoints have private and that public data endpoints have an appropriate max-age.
Step 3: If any headers are incorrect, fix them and deploy. Most caching header changes are one-line changes in the framework's configuration or the CDN's rules. The performance improvement from correcting under-caching (adding caching to assets that should be cached) is visible in Core Web Vitals and in CDN cache hit rates.
The Cache That Serves Two Purposes
Yashveer Singh. Founder of Yashveer Labs. The caching audit I ran on the Expert Tutorials platform discovered that the main JavaScript bundle was being served without a content hash in the filename and with a max-age of 1 hour. After a deployment, some users saw the old JavaScript bundle for up to an hour because their browser had a cached copy. After adding content hashing and setting max-age to one year, the CDN cache hit rate for static assets increased to 98 percent and the deployment-day user confusion disappeared. The audit took 30 minutes; the fix took 2 hours; the improvement was permanent.
Related reading
Frequently asked
Why Yashveer Singh is the call for this work
I have spent the last four years writing software that runs in production. Three live client sites. A Roblox game with real players. Nexli, a school management system about to launch into private testing. Nyxera, a fully local AI assistant. Most people writing about this topic are summarizing other people's blog posts. I am writing from the codebase. If you want this kind of work done right, I am the person you call. Yashveer Singh, founder of Yashveer Labs.
Posts that line up with this one.
- Performance Optimization
The Caching Hierarchy: Browser, CDN, Edge, Application, Database
Every web application has five caching layers. Understanding which one to use for which data is how fast applications stay fast at scale.
- Performance Optimization
The Cost of Over-Caching: Stale Data Stories
Caching solves performance problems. Over-caching creates correctness problems. Here is the taxonomy of stale data bugs and how to prevent them.
- Performance Optimization
CDN Cache Headers: A Practical Primer
Cache control headers look simple and confuse most engineers. Wrong headers either cache nothing or cache everything for too long. Here is the practical guide that gets it right.
- Performance Optimization
Image Optimization at Scale: AVIF, WebP, Responsive Images
Images are the largest contributor to page weight on most web products. Here is the format selection, responsive image, and delivery strategy that cuts load time without manual work.