You've put your site behind a CDN, changed the CSS file, but the user's browser is still showing the old version. Or worse: after a price change, the product page returns the old number for some users and the new number for others. The problem usually isn't the CDN; it's that you don't know exactly what the edge is storing and with what key. This article is what you need to know to predict CDN cache behavior, rather than approaching it with guesswork and trial and error.
What the CDN caches by default
The short answer: less than you think. Most CDNs only cache GET and HEAD responses, and treat any response with status code 200, 203, 204, 206, 300, 301, 404, or 410 as "cacheable." Responses to POST, PUT, and DELETE are never cached, even if you set a Cache-Control header. Many people don't know this and spend hours hunting for bugs.
The second point, which causes more trouble than the first: if a response has no caching headers at all, the CDN decides based on a default policy. This policy differs between providers. Some don't cache headerless responses at all; others cache them with a short default TTL. If you count on default behavior, the day the provider changes its policy, your site's behavior will change without warning.
The cache key isn't just the URL
The default key almost always includes the method, path, and query string. That means /product?id=12 and /product?id=13 are two separate entries, but /product?id=12&utm_source=x becomes a third entry too. This is where your cache volume multiplies unnecessarily and your hit ratio drops. The standard solution is to ignore marketing parameters in the cache key; almost all CDNs have a configurable query string list for this.
The Vary header makes the key more complex. If your response has Vary: Accept-Encoding, the gzip and brotli versions are stored separately. If you set Vary: User-Agent, you've effectively disabled caching, because every browser creates a different key and the hit ratio drops to near zero. In practice, I only like two values on Vary: Accept-Encoding and, in special cases, Accept-Language. Remove the rest.
How headers actually make the decision
Keep the priority order in mind, because CDNs implement this same order:
Cache-Control: no-storetakes precedence over everything. The response is stored neither at the edge nor in the browser.Cache-Control: privatemeans only the browser can cache it, not the edge. For pages that depend on the user's cookie.Cache-Control: public, max-age=31536000, immutablefor versioned files likeapp.4f2c1a.js.s-maxageis only for shared (edge) caches and takes priority overmax-age.Expiresis only used whenCache-Controlis absent. If both are present,Cache-Controlwins.
A real line I write for versioned static files:
Cache-Control: public, max-age=31536000, s-maxage=31536000, immutable
And for HTML that might change at any moment:
Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=300, stale-if-error=86400
The number s-maxage=60 means the edge serves the old version for at most one minute. stale-while-revalidate=300 means for up to five minutes after expiration, serve the old response immediately and refresh in the background. stale-if-error=86400 means if the origin errors out for a full day, the edge serves that same old version. These two directives are the difference between "site is up" and "502 page" during an origin outage.
This is where people go wrong
The most common mistake I've seen: setting Cache-Control: no-cache under the impression that it means "don't cache." This header means "cache it, but ask the origin before every use." The result is that the edge makes a round trip to the origin for every request, TTFB goes up, and origin load multiplies, while the team thinks they've completely turned off caching. If you really don't want it cached, write no-store. The difference between these two words is the difference between a useless CDN and an efficient one.
Mistake number two: Set-Cookie on static responses. Some CDNs mark a response as uncacheable the moment they see Set-Cookie in it. If you're setting cookies on JS and CSS files, you're sabotaging the cache with your own hands.
Selective invalidation, not purging the whole cache
When content changes, you have two options. The first is a full purge: everything is cleared from the edge and a wave of requests hits the origin all at once. If your site has high traffic, this knocks the origin down. The second is selective invalidation: purge only that path or that pattern.
Most CDNs have three types of purge: by exact URL, by prefix or wildcard, and by tag or surrogate key. The third type is the most precise. If you set the header Cache-Tag: product-12, category-shoes on the response, you can later invalidate only product-12 without touching the rest of the category. This is the only method that truly works in large stores.
| Method | Precision | Cost | Where it fits |
|---|---|---|---|
| Full purge | Zero | Heavy wave to origin | Global layout change, rarely |
| Purge by URL | High | Light | A few specific pages |
| Purge by prefix | Medium | Medium | An entire path like /blog/* |
| Purge by tag | High | Requires code changes | Store, data-dependent content |
A real limitation that's rarely mentioned: invalidation isn't instant. It takes anywhere from zero to a few seconds for the command to reach all edges, and on large geographic networks this number can be higher. If your business logic depends on "immediately," don't rely on edge caching; take that path out of the cache.
A versioning pattern that reduces invalidation
The best invalidation is the invalidation you don't need. Tie the filename to its content: main.a91f3c.css instead of main.css. Now every change creates a new URL and the old file expires on its own with a one-year TTL. Cache HTML briefly, assets for a long time. This combination both gives a high hit ratio and turns invalidation into a rare operation.
If your infrastructure is on ServerNet's cloud server or cloud infrastructure (IaaS), you can combine this pattern with a load balancer with health check and SSL in front of the origin so that even a full purge wave doesn't knock the origin down. For serving versioned assets, object storage is a more sensible option than the web server's disk, because you set the headers once and never touch them again.
How to measure actual cache behavior
Look at the response headers, not what the docs say. Run this command:
curl -sSI -H 'Accept-Encoding: gzip' https://example.com/main.css | grep -iE 'cache-control|age|x-cache|cf-cache|vary'
The Age header is the golden number. If Age: 0, the response came fresh from the origin. If Age: 47, this response has been sitting at the edge for 47 seconds and you got a hit. Make two requests back to back; the second should give a larger Age. If it doesn't, caching isn't working and you should look for Set-Cookie or an extra Vary.
To measure the real hit ratio, don't rely on the CDN dashboard; count the origin logs. If the number of requests reaching the origin hasn't changed after enabling caching, chances are all traffic is passing through the cache without being stored. This situation can be detected with cloud monitoring and alerts on the origin request rate; a simple alert on a sudden spike in this number catches many outages before they happen.
If you're also changing the server configuration after adjusting cache settings, take a snapshot of your cloud space before any risky change. Recovering from a snapshot takes minutes; manually rebuilding the configuration takes hours.
Frequently asked questions
What's the difference between no-cache and no-store in CDN cache behavior?
no-cache allows storage but requires validation from the origin before every use, so origin traffic doesn't decrease. no-store doesn't store at all and no version remains at the edge or in the browser. For sensitive data like a checkout page, no-store is correct.
Why do some users still see the old version after a purge?
Because a purge only clears the edge cache, not the user's browser cache. If a file was stored in the browser with max-age=31536000, it will show that same version until it expires. The permanent solution is renaming files based on a content hash, not repeated purging.
Does the CDN cache 404 responses too?
Yes, in many implementations a 404 response is cacheable. If you temporarily removed a page and then restored it, the edge may still return 404. To prevent this, set a short Cache-Control like max-age=60 on 404 responses.
How does the Vary header hurt the hit ratio?
Any value in Vary becomes part of the cache key. Vary: User-Agent effectively creates a separate entry for every browser version and drives the hit ratio to near zero. Keep only Accept-Encoding wherever possible.
The next step is simple: right now, use curl -sSI to grab the headers of a static file and an HTML page, and put Age and Cache-Control side by side. If the Age number doesn't grow on the second request, that's where your caching problem is, not in the CDN settings.