Your app can feel fast in one city and slow everywhere else. The code did not change. The database did not change. The difference is often the path each request travels before the browser can paint the page.
A CDN shortens that path by keeping reusable responses near users. It can serve JavaScript bundles, CSS, images, public pages, and selected API responses from an edge location instead of sending every request back to the origin server.
The hard part is not putting files on "global servers." The hard part is deciding what is safe to reuse, how long it stays fresh, what changes the cache key, and how quickly updates should replace old content.
TLDR
- A CDN is a shared cache near users that reuses HTTP responses instead of hitting your origin every time.
- It speeds up web apps by cutting distance, reusing responses, shielding the origin, and refreshing content in the background.
- It only works when your URLs,
Cache-Controlheaders, cache keys, and invalidation strategy make freshness rules explicit. - Most CDN bugs are not the CDN breaking. They are unclear cacheability, unstable cache keys, or accidental personalization.
What problem a CDN solves#
A CDN solves a simple but expensive problem: many users ask for the same public resources, but without caching every request has to travel back to the same origin. That creates unnecessary latency for users and unnecessary work for your infrastructure.
Think of a CDN as a distributed memory layer for HTTP. It sits between the browser and your origin, checks whether it already has a safe copy of the response, and returns that copy when it can.
The mental model is:
| Question | CDN decision |
|---|---|
| Have I seen this request before? | Check the cache key. |
| Is the cached response still fresh? | Check TTL and validation headers. |
| Can this response be shared between users? | Check cache policy and personalization signals. |
| What happens after the response gets old? | Revalidate, serve stale, or fetch from origin. |
This is why CDN work is really cache design work. The edge can only move fast when the application gives it clear rules.
How a CDN request flows#
A CDN-delivered request has three possible outcomes: hit, miss, or revalidation. A hit means the edge already has a fresh response. A miss means the edge must ask the origin. Revalidation means the edge has a response, but it needs to confirm whether the origin has changed before serving or refreshing it.
The core loop looks like this:
- A browser requests
/app.84f2c1.js. - DNS and routing send the request to a nearby CDN edge.
- The edge checks whether it already has a matching cached response.
- On a cache hit, the edge returns the response directly.
- On a miss, the edge forwards the request to the origin server.
- The origin returns the response with caching headers.
- The edge stores the response if the policy allows it.
- Later users can receive the cached response without touching the origin.
The same loop, viewed as a request traveling between the browser, the edge, and the origin, looks like this:
That loop is why CDNs are useful for web apps. JavaScript bundles, CSS, fonts, images, generated HTML, documentation pages, and some API responses can be reused across users. Authentication-specific dashboards and payment pages usually cannot.
For a generated HTML page or API response, the path is similar, but the rules are stricter. The CDN must know whether the response is public, whether it varies by user, and how stale it can become before the product experience is wrong.
Modern platforms expose this behavior through response headers. Vercel, for example, documents x-vercel-cache values such as HIT, MISS, STALE, and REVALIDATED, and describes STALE as a cached response that is no longer fresh while a background update is triggered (Vercel response headers).
Cloudflare uses similar cache statuses and also documents how stale content can be served while revalidation happens in the background when stale-while-revalidate is present (Cloudflare revalidation docs).
Why this makes a web app feel faster#
CDNs improve the felt speed of a web app in three ways: they reduce physical distance, reduce origin work, and improve repeat-load behavior. Google explains Largest Contentful Paint as the time from navigation until the largest image or text block in the viewport is rendered, and its LCP guidance recommends reducing resource load duration through efficient caching and nearby delivery with a CDN (web.dev, "Optimize Largest Contentful Paint").
Distance still matters. A user in Mumbai should not wait on every static asset from an origin in Virginia if the same asset can be served from a nearby edge. Even when bandwidth is good, connection setup, TLS negotiation, packet travel, and origin queueing add latency.
Caching matters even more on repeat views. The fastest network request is the one that does not need the origin. If a browser cache or CDN edge can reuse a response, the app avoids backend work and shortens the path from navigation to pixels.
Each layer in front of the origin can short-circuit the request, so the response travels back from the first layer that holds a fresh copy:
Images are a common example. Google notes that image CDNs can often reduce image file size by 40-80% through transformations such as resizing, format selection, and compression (web.dev, "Use image CDNs to optimize images"). That is not because "CDN" is magic; it is because the delivery layer can serve the right image variant for the device and browser.
Performance rule of thumb
Use the CDN for things that many users can share: versioned assets, public images, public documentation, product pages, blog posts, and stable API responses. Be cautious with anything personalized, permissioned, or dependent on cookies.
Cache keys decide whether two requests are the same#
The cache key is the identifier a CDN uses to decide whether a request can reuse an existing response. If two requests produce the same cache key, they can share the same cached object. If the cache key differs, the CDN treats them as separate objects even if the origin would return identical bytes.
Cloudflare describes a cache key as the identifier used for a file in cache, with the default key including the full URL and relevant headers such as Origin for CORS behavior (Cloudflare cache keys). That small detail explains many confusing CDN bugs.
For example, these can become different cache entries:
/blog/how-cdns-deliver-web-apps
/blog/how-cdns-deliver-web-apps?utm_source=newsletter
/blog/how-cdns-deliver-web-apps?utm_source=twitter
If analytics parameters are included in the cache key, the CDN may store multiple copies of the same page. That lowers the cache hit rate and sends more requests back to the origin. If query parameters actually change the response, they should remain part of the key. If they only track campaigns, they should usually be ignored or normalized by the CDN.
Common cache key inputs include:
| Input | Why it matters |
|---|---|
| Hostname | www.example.com and app.example.com may serve different content. |
| Path | /pricing and /docs are different resources. |
| Query string | Sometimes changes content, sometimes only tracks analytics. |
| Method | GET is cacheable far more often than POST. |
| Headers | Accept, Origin, or locale headers can change the response. |
| Cookies | Usually dangerous for shared caches unless explicitly controlled. |
The safest strategy is to make URLs deterministic. If the content is the same, the URL should be the same. If the content changes, the URL or revalidation policy should make that clear.
What should be cached#
The best CDN candidates are public, reusable, and easy to invalidate. Static assets are the simplest because their filenames can change when their content changes. HTML and API responses can also be cached, but they need more careful freshness rules.
Here is a practical breakdown:
| Resource type | Cache strategy | Why |
|---|---|---|
| Fingerprinted JS/CSS | Long browser and CDN cache with immutable | File name changes when content changes. |
| Public images | Long CDN cache, often transformed per device | Images are large and frequently reused. |
| Blog/docs HTML | Short-to-medium CDN cache with revalidation | Public content changes occasionally. |
| Product listing API | CDN cache with short TTL and stale window | Reusable but freshness matters. |
| Authenticated API | Usually private or no-store | Response depends on the user. |
| Checkout/account pages | Avoid shared CDN caching | Sensitive and stateful. |
The important distinction is not static versus dynamic. It is public versus private, stable versus volatile, and reusable versus user-specific.
For example, a generated marketing page can be cached at the CDN if every visitor sees the same HTML. A JSON endpoint can be cached if it returns a public catalog. A dashboard endpoint should not be shared across users unless the response is carefully partitioned and protected.
Cache-Control headers are the contract#
Cache-Control tells browsers and shared caches how to store and reuse a response. MDN describes it as the HTTP header that carries caching directives for browsers and shared caches such as proxies and CDNs (MDN Cache-Control reference).
These are the directives most web app teams need first:
| Directive | Meaning |
|---|---|
public | Any cache may store the response. |
private | Browser cache may store it, shared caches should not. |
no-store | Do not store the response. Use for sensitive data. |
no-cache | May store, but must revalidate before reuse. |
max-age=60 | Response is fresh for 60 seconds. |
s-maxage=300 | Shared caches can use a different TTL than browsers. |
immutable | Fresh response will not change during its lifetime. |
stale-while-revalidate=86400 | Serve stale content while refreshing in the background. |
stale-if-error=86400 | Serve stale content if the origin fails. |
For fingerprinted assets:
Cache-Control: public, max-age=31536000, immutable
This works because a build output such as app.84f2c1.js should never change in place. When the content changes, the filename changes too.
For public HTML that can be slightly stale:
Cache-Control: public, max-age=300, stale-while-revalidate=86400
This says the response is fresh for five minutes. After that, caches may serve the stale response while checking the origin in the background for up to one day.
For private account data:
Cache-Control: private, no-store
The exact policy depends on the product, but the principle is consistent: shared caches should not store user-specific data.
Do not cache by hope
If a response includes user identity, permissions, account state, tokens, or private business data, assume it should not be stored in a shared CDN cache unless you have an explicit isolation strategy.
Stale-while-revalidate makes fast and fresh less opposed#
stale-while-revalidate lets a cache serve an expired response immediately while it refreshes the content in the background. MDN describes this as reusing a stale response while revalidating it, which hides the latency penalty of revalidation from the user (MDN Cache-Control reference).
This pattern is useful for content that changes, but not so urgently that every visitor must wait on the origin. Blogs, documentation, product information, pricing snippets, public search pages, and product catalogs often fit this model.
The timeline looks like this:
0-5 minutes: response is fresh
5 minutes later: response is stale
next request: CDN serves stale response immediately
background: CDN asks origin for the latest version
after revalidation: CDN stores the fresh response
following requests: fresh response is served
Cloudflare documents that when an origin includes stale-while-revalidate, Cloudflare can serve an expired asset while fetching a fresh copy from the origin, using validators such as If-Modified-Since and ETag to avoid refetching unchanged content (Cloudflare revalidation docs).
Vercel also supports targeted CDN caching through headers such as Vercel-CDN-Cache-Control, CDN-Cache-Control, and Cache-Control, with platform-specific priority in that order (Vercel cache control headers).
That means a Next.js app can use different freshness windows for browser caches and CDN caches. For example, a browser may revalidate quickly while the CDN keeps a longer shared stale window.
Invalidation is how you change your mind#
Invalidation is the process of telling the CDN that cached content should no longer be considered fresh. Without an invalidation strategy, teams either cache too cautiously or purge too broadly. Both are expensive: one wastes performance, the other creates origin spikes.
There are four common invalidation patterns:
- Versioned URLs: Change the filename when content changes, such as
app.84f2c1.js. - TTL expiry: Let cached content expire naturally after a defined time.
- Path purge: Remove one path, such as
/pricing. - Tag purge: Invalidate a group of related responses, such as
product-123.
Versioned URLs are best for static assets. TTLs are best for content that can tolerate short staleness. Path purges are useful for known pages. Tag purges are better when one data change affects many pages.
Vercel documents cache tag invalidation as the recommended option for many cases because it marks cached content as stale and lets the next request receive stale content while revalidation happens in the background. It also warns that deleting cache entries can force foreground origin requests and contribute to a cache stampede (Vercel CDN cache purging).
That distinction matters in production. "Delete every cache entry now" sounds simple, but if thousands of users request the same pages immediately afterward, the origin may receive a sudden wave of expensive work.
Where CDN setups go wrong#
Most CDN bugs are not caused by the CDN being broken. They come from unclear cacheability, unstable cache keys, missing headers, or accidental personalization. The edge does exactly what it was told, but the instructions were incomplete.
Common failure modes include:
- Caching user-specific responses: A private response is accidentally stored in a shared cache.
- Fragmented cache keys: Tracking query strings create many copies of the same page.
- Short TTLs for immutable assets: Fingerprinted JS and CSS keep revalidating for no reason.
- Long TTLs for unversioned content: Users keep seeing old files because the URL did not change.
- Missing
Varyawareness: The CDN serves the wrong language, image format, or CORS variant. - Purge-all deployments: Every deploy wipes useful cache and pushes traffic to the origin.
- Ignoring image connection cost: A third-party image CDN can add extra DNS, TCP, and TLS setup before the LCP image starts loading.
Google's image CDN guidance calls out this last issue directly: images served from a cross-origin CDN can increase connection setup time, so proxying through the primary origin or using preconnect can reduce delay for important images (web.dev, "Use image CDNs to optimize images").
How to debug a CDN response#
Debugging a CDN starts with reading the response headers, not guessing. Cache headers tell you what the origin asked for, while platform headers tell you what the edge actually did.
Use a command like this:
curl -I https://example.com/blog/how-cdns-deliver-web-apps
Look for headers such as:
cache-control: public, max-age=300, stale-while-revalidate=86400
etag: "abc123"
age: 124
x-cache: HIT
x-vercel-cache: HIT
cf-cache-status: HIT
vary: Accept-Encoding
The exact names vary by CDN. The interpretation is similar:
| Header | What to check |
|---|---|
cache-control | Is the response allowed to be cached? For how long? |
age | How long has the response been in a shared cache? |
etag / last-modified | Can the cache revalidate efficiently? |
vary | Which request headers create separate variants? |
x-cache / cf-cache-status / x-vercel-cache | Did the CDN hit, miss, serve stale, or revalidate? |
set-cookie | Is the response accidentally personalized? |
If you see repeated misses for a public static asset, inspect the cache key inputs. If you see stale content for too long, inspect TTLs and invalidation. If you see private data cached, stop and fix the headers before tuning performance.
A practical CDN policy for a modern web app#
A good CDN policy starts boring and explicit. Cache immutable assets aggressively, cache public pages with bounded staleness, cache public APIs only when the response is reusable, and avoid shared caching for private user data.
Here is a reasonable starting point:
| Surface | Example policy |
|---|---|
| Build assets | public, max-age=31536000, immutable |
| Public images | Long CDN TTL, versioned URLs or image transformation cache |
| Blog/docs pages | public, max-age=300, stale-while-revalidate=86400 |
| Public catalog APIs | Short max-age, longer stale window, tag invalidation |
| Authenticated APIs | private, no-store or no shared CDN cache |
| Admin pages | no-store unless carefully isolated |
For a Next.js-style deployment, the framework and platform may generate some of this for you. Still, it is worth understanding the underlying model because bugs usually appear at the boundary: custom API routes, proxied backends, external images, cookies, rewrites, and manual headers.
If you are designing the broader architecture of an AI or product system, see our notes on system design for AI products. CDN behavior is one part of that larger reliability and latency story.
Key Takeaways#
A CDN is not just a faster pipe. It is a distributed decision system for HTTP responses. It decides whether two requests are the same, whether a response is public, whether stale content is acceptable, whether the origin must be contacted, and how quickly a change should spread.
When the rules are clear, CDNs make web apps feel local, reduce origin pressure, improve repeat visits, and help important resources such as images and scripts arrive sooner. When the rules are vague, they create hard-to-debug freshness, personalization, and cache-miss problems.
The practical path is simple: version your static assets, write explicit Cache-Control headers, keep cache keys stable, use stale revalidation for public content, invalidate precisely, and inspect headers before guessing.
FAQ#
Is a CDN only for static files?
No. Static files are the easiest CDN use case, but public HTML, generated pages, images, and selected API responses can also be cached. The real question is whether the response is reusable across users and whether the product can tolerate the freshness window.
What is the difference between browser cache and CDN cache?
Browser cache lives on one user's device. CDN cache lives in shared edge locations and can serve many users. A response can have different policies for each layer through directives such as s-maxage or platform-specific headers like CDN-Cache-Control.
Should every API response be cached at the edge?
No. Cache public and reusable API responses only when freshness rules are clear. Do not share-cache authenticated account data, permissioned responses, checkout state, or anything that depends on private cookies unless the isolation model is explicit and tested.
Why does my CDN still miss after I added caching headers?
Common reasons include query-string fragmentation, cookies in the request, Set-Cookie in the response, Vary creating many variants, platform rules overriding origin headers, or the response being considered uncacheable by default. Start by inspecting response headers and CDN cache status headers.