
The first time you open a site, the page loads in two seconds. You close the tab, come back to the same page five minutes later and it opens almost instantly. Nothing on the site has changed, and your connection is the same. The only difference is that on the second visit, the browser does not have to request most of those files again.
Two separate mechanisms create this difference, and in practice they are constantly confused. A CDN shortens the distance: you still send the request to a server, just a closer one. Caching eliminates the request entirely: because the file is already on the device or the edge server, there is no trip to the network at all. The second visit is fast largely because of the latter. The former is what rescues the first visit.
This distinction is also the foundation of the "aggressive caching" debate. Aggressive settings extend how long a file can be used without being requested again. The gain is real, but most guides gloss over the cost: once you give a file a long lifetime, your ability to take it back is not the same at every layer.
How a CDN shortens the distance
A CDN (Content Delivery Network) is a distributed network that keeps copies of a site's files on edge servers in different geographic locations. When a user sends a request to the site, it does not go directly to the main server (the origin) but lands on the network's edge location closest to the user. If the requested response is in that edge server's cache (a cache hit), the response is returned from there and the origin is never touched. If not (a cache miss), the edge server fetches the content from the origin once, passes it to the user and stores it for subsequent requests.
A CDN is so effective because the cost of distance is not paid just once. On a new HTTPS connection, DNS resolution, the TCP handshake and the TLS handshake require several round trips between the browser and the server before the first byte of HTML arrives. Because light travels roughly 200,000 kilometers per second in fiber, every 1,000 kilometers of distance adds about 10 milliseconds to a single round trip. A number that looks small on its own gets multiplied by three or four round trips. An edge server does not change the number of round trips; it shortens the distance each one travels, which is why the saving grows with every round trip.
What a CDN cannot do is just as decisive: a CDN does not speed up your origin server. If generating a page's HTML takes 800 milliseconds because of database queries and the HTML is not cached, the CDN does not eliminate those 800 milliseconds; it even adds another stop along the way. A CDN's speed contribution comes from cases where the request never reaches the origin at all. In other words, a CDN's value depends on how well you write your cache rules. Providers such as Cloudflare, Fastly and Akamai share the same basic architecture; the differences between them lie in the size of the network and how cache rules are defined.
Three cache layers and who can purge each one
Before a response reaches the user, it can be stored in three independent places. These layers read the same headers, but they don't answer to you in the same way.
| Layer | Where it lives | Who it applies to | Can you purge it |
|---|---|---|---|
| Browser cache | On the user's device | Only that user | No |
| CDN cache | On edge servers | All visitors | Yes, within seconds |
| Server cache | On your own server | All visitors | Yes |
The last column in the table is the basis for every decision in the rest of this article. If you wrote the wrong content to the CDN cache, you can undo it within a few seconds with a purge request. The server cache is already on your machine. But once you have told a user's browser "don't request this file again for a year", you have no mechanism to reverse that decision. Unless the user clears their cache themselves or the time runs out, that file stays there.
The server cache solves a different problem from the other two. It reduces the cost of generating HTML by serving ready-made output instead of going to the database. It may seem that a site behind a CDN does not need a server cache, but every request that cannot be cached at the edge (personalized pages, pages whose cache has expired, new edge locations) still falls through to the origin. The two layers do not replace each other.
What Cache-Control headers actually say
The origin server decides how long a response should be stored, and it says so with the Cache-Control header. Both the browser and the CDN read the same header. In practice, you only need a handful of directives.
| Directive | Plain meaning |
|---|---|
| max-age=N | This response is considered fresh until N seconds after it was generated and can be used without asking again. |
| s-maxage=N | A lifetime for shared caches (CDNs) only. If present, it replaces max-age on the CDN side; the browser ignores it. |
| public | Can be stored in a shared cache. |
| private | Can only be stored in the user's own browser; the CDN does not store it. |
| no-cache | Can be stored, but must be revalidated with the server before every use to check it is still valid. |
| no-store | No cache should store this response. |
| immutable | This file will not change while it is fresh, so do not send a revalidation request even if the page is reloaded. |
| stale-while-revalidate=N | For N seconds after expiry, the stale copy can be served while the update happens in the background. |
The most misread directive on this list is no-cache. Despite its name, it does not say "don't cache". It allows the response to be stored and only requires that it be validated before each use. During validation, the browser sends the ETag value it holds in the If-None-Match header; if the content has not changed, the server returns 304 Not Modified without sending a body. So with no-cache the file is not downloaded again; only one round trip is spent. If you really mean "never store this", the directive you need is no-store.
The second common misconception concerns when the max-age counter starts. The counter starts not when the response reaches you but when the origin generated it. If a response sat in the CDN for 100 seconds, the CDN reports that in the Age header, and those 100 seconds are subtracted from the browser's freshness lifetime. In a setup with long s-maxage values, the file that reaches the browser may have a much shorter lifetime than you calculated.
The third is the one that does the most damage quietly: not sending Cache-Control does not mean "no caching". Because HTTP is designed to cache as much as possible, when the header is missing, clients apply heuristic caching and store the response at their own discretion based on the Last-Modified date. In that case, the client, not you, decides how long the file is kept. That is why giving every response an explicit Cache-Control is the basic step that comes before any aggressive settings.
Exactly what aggressive caching puts at risk
The word "aggressive" is used in this context as if it were a virtue in itself, and that is misleading. Being aggressive means giving a file a longer lifetime than it actually deserves. The gain is measurable, and the risks fall under four headings.
The first is stale content. When a page that was given a long lifetime is updated, the user still sees the old version. In e-commerce, the consequence is concrete: a user who sees the old price after a sale has ended, or thinks a sold-out product is in stock. When fields like price and stock freeze inside the cached body of the page, it stops being a technical misconfiguration and becomes a direct commercial problem.
The second is irreversibility. You fix a mistake made on the CDN with a purge; you cannot fix a mistake made in the browser. If you gave a file with a fixed name a one-year lifetime and changed its content the following week, there is nothing you can do for users who have already downloaded it. A purge request clears the CDN; it does not touch the copy on the user's disk.
The third is version mismatch. After a release, a user may end up with new HTML combined with old JavaScript or old CSS. The page opens, but the layout is broken, a button doesn't work or a component doesn't appear at all. The worst part of this bug is that it does not show up on the developer's screen: you bypass the cache with a hard refresh, and users don't.
The fourth, and most serious, is a personalized response being written to a shared cache. If a cart, account or order page is marked public, the CDN stores that response and may serve it to another user requesting the same URL. That is not a speed issue; it is a data leak. Marking every personalized response private or no-store is non-negotiable.
What these four risks have in common: setting up a cache is easy; invalidating it is hard. Aggressive settings are safe only when the invalidation problem is eliminated from the start. The next section explains exactly that.
Why caching HTML and caching static assets are not the same job
Modern build tools add a hash generated from the content to the names of JavaScript and CSS files, as in app.9f2c1b.js. The moment the content changes, the hash changes, the file's URL changes and the browser downloads it as a new file it has never seen before. There is no need to invalidate the old file here, because no one will ever request the old URL again. Because URL and content map one to one, it is safe to give these files a one-year lifetime and add immutable.
Cache-Control: public, max-age=31536000, immutable
HTML has no such option. The URL of an article is permanent: users bookmark it, other sites link to it and Google indexes it under that URL. You cannot change the URL because the content changed. In other words, HTML is the one resource type whose content changes while its URL stays fixed, and it cannot escape the invalidation problem. That is why a long lifetime that is correct for static files becomes a trap for the same site's HTML.
The solution is to give two layers two different lifetimes instead of choosing a single one. Keep the layer you can purge aggressive and the layer you can't purge short. s-maxage makes exactly this possible: it sets the lifetime on the CDN side separately, and the browser ignores it.
Cache-Control: public, max-age=0, s-maxage=600, stale-while-revalidate=86400
With this line, the browser treats the response as stale immediately and sends a revalidation request on the next visit; if the content has not changed, it gets a 304 and uses its copy without downloading. The CDN, meanwhile, serves the same response directly for ten minutes, and for one day after that it keeps serving the stale copy while updating in the background. When you update an article, you purge the CDN cache and don't have to wait ten minutes. And on the user's side, you haven't made any commitment you can't take back.
Starting settings by resource type
The values below are not universal truths but reasonable starting points. The right number depends on how often you publish and whether you can set up automatic purging.
| Resource type | Cache-Control | Rationale |
|---|---|---|
| JS and CSS with a content hash in the name | public, max-age=31536000, immutable | The URL changes when the content changes, so no invalidation is needed. |
| Images and fonts with fixed names | public, max-age=604800 | One week, a reasonable upper limit for a commitment you can't reverse. |
| HTML that is the same for everyone | public, max-age=0, s-maxage=600, stale-while-revalidate=86400 | The aggressive layer is the one that can be purged. |
| Personalized page (cart, account) | private, no-store | Ending up in a shared cache is a data leak. |
| API response that is the same for everyone | public, max-age=0, s-maxage=60 | A short edge lifetime significantly reduces origin load at peak times. |
When giving a file a lifetime, the first question to ask is not "how many seconds". The first question is: when the content changes, does this file's URL change too? If the answer is yes, you can be as aggressive as you like. If not, you are giving up the right to change that content for the duration you set. For images with fixed names, the practical way out is to upload the new version under a new name instead of overwriting the file, and update the references.
The real impact on TTFB and LCP
Cache settings show their contribution to Core Web Vitals most clearly in TTFB. For an HTML response served from cache at the edge, server processing time disappears entirely, and only network distance remains. Because TTFB is the first component of LCP, this gain carries straight over to LCP: when you are working on getting LCP under 2.5 seconds, the single biggest item on most sites is server response time.
Beyond that, the effect should not be overstated. A CDN changes little if the server is not what delays LCP. If the LCP image is discovered late, if stylesheets block rendering or if a script keeps the main thread busy, a response arriving in 40 milliseconds won't save the result. In that case, the next job is fixing the rendering chain, for example by inlining critical CSS. The protocol layer is a separate gain as well: edge servers usually provide HTTP/3 and QUIC support regardless of the origin server, further reducing the cost of setting up a connection.
On the measurement side, there is a delay to keep in mind. Core Web Vitals assessment is based not on lab tests but on field data collected from real users and accumulated over a rolling 28-day window. When you fix a cache setting today, a single measurement in a testing tool improves immediately, while the 75th percentile value in the field data takes weeks to settle. Changing the setting and checking the report the next day gives a misleading result.
How to verify the setting is working
The fastest way to see that the configuration is actually applied is to look at the response headers. Open a request's response headers in the Network tab of your browser's developer tools and look for three things.
- Does the Cache-Control line carry the value you expect? The rule in the server configuration and the live response often diverge because of a layer in between.
- Is there an Age header, and is it greater than zero? Age tells you how long the response waited in a shared cache; its presence is direct proof that the request did not go to the origin.
- On the second visit, does the request return 304 instead of 200, or does the size column show it was read from disk? Both mean the response was not downloaded again.
A common mistake when running this test is using a hard refresh. A hard refresh deliberately bypasses the cache, so you see a fresh download every time and conclude that the setting isn't working. The right test is to reopen the page through normal navigation.
Frequently Asked Questions
Does Googlebot see different content when you use a CDN?
No. The response served from the edge is a copy of the response the origin would serve, and from a crawling perspective there is no difference between them. What needs attention is not the content but the configuration: if a rule exempts bot traffic from the cache or treats it differently, the bot cannot benefit from the CDN's speed advantage. You also need to make sure the edge server's security layer does not block bot requests.
Can I clear a user's browser cache remotely?
No. That copy stays on the user's device until the lifetime you gave it expires. The only thing you can do is change the file's URL and update the HTML to point the user to the new URL. That also explains why HTML is not given a long lifetime: because HTML is short-lived, it is the only layer that can carry new URLs.
Do I need a CDN if I already have server-side caching?
The two solve different halves of the problem. Server caching eliminates the cost of generating the page but does not shorten the distance between the user and the server. A CDN shortens the distance but still sends uncacheable requests to the origin. With only server caching on a distant server, the response is generated quickly but arrives slowly; with only a CDN in front of a slow site, the response travels quickly but you still wait for it to be generated.



