Cloud & Infrastructure

Configuring a CDN Without Breaking a Dynamic Site

If after enabling a CDN logged-in users are shown incorrectly or the shopping cart empties, the problem is the cache policy. Here we explain exactly what should and shouldn't be cached.

Cloud & Infrastructure

You've put your site on a CDN and now a user who has logged in sees the page in guest mode. Or worse: one person's shopping cart is shown to the next person. If you open the server log, you'll see requests to /account answered with status code 200 (HIT), when they should be coming from the origin. This is exactly where CDN configuration separates itself from "enabling a CDN."

The problem isn't the CDN itself. It's that you've told the edge what to cache, and that instruction doesn't match the reality of your dynamic application.

The dangerous default: caching everything based on file extension

Most CDN panels have a ready-made rule that says "cache static files." The definition of "static" in that rule is usually based on extension: .css, .js, .jpg, .woff2. So far so good. The disaster begins when you apply this rule to paths that have no extension but are dynamic behind the scenes.

A URL like /api/v1/cart has no extension, so at first glance it doesn't look static. But if your cache rule is based on "anything under /assets/" and someone mistakenly puts an endpoint under that same path, from that moment on personalized responses get cached.

The correct rule is: decide caching based on the origin response header, not based on the path. If your application returns Cache-Control: private, no-store, the CDN must respect it.

Which headers determine what gets cached

There are three main headers, and their order of importance is fixed.

  • Cache-Control: the most important. The value public, max-age=31536000, immutable for versioned files, and private, no-store for anything that depends on the user session.
  • Vary: if the response differs based on Accept-Encoding or Accept-Language, it must be declared. Without it, the gzip version reaches a browser that doesn't understand gzip.
  • Set-Cookie: if it's in the response, cautious CDNs won't cache that response. If your CDN ignores this, that's exactly where the shopping cart problem is born.

A practical tip: in the origin Nginx, put this line for personal paths:

location /account/ {
    add_header Cache-Control "private, no-store, max-age=0";
    add_header Vary "Cookie, Accept-Encoding";
}

And for versioned static files:

location ~* \.(css|js|woff2|webp)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

The number 31536000 means one year. This number is only safe when the filename includes a content hash, like app.4f9c2a.js. If the filename is fixed, caching for a year means the user sees the old version for a year.

Query string: the decision most sites get wrong

The default for most CDNs is to include the entire query string in the cache key. That means /style.css?v=1 and /style.css?v=2 are two separate objects at the edge. This behavior is safe but lowers the cache hit rate.

The second mode is to ignore the query string entirely. This raises the cache hit rate but creates a subtle bug: if query parameters affect the content, the wrong response gets served.

The third mode, which I choose as the default, is a whitelist. Only parameters that actually change the content stay in the cache key:

# only these parameters stay in the cache key
v, ver, version, lang, currency

Parameters like utm_source, gclid, and fbclid should be removed. If they aren't, every ad campaign creates a fresh copy of the same page at the edge and your cache hit rate drops for no reason. In one project this exact thing happened: the cache hit rate went from 91% to 34% because email links all had utm_.

Cookies and the user session

If your CDN is set to "cache unless a cookie is present," two cases arise. The guest user gets cached, the logged-in user doesn't. This seems logical and in practice works for news and e-commerce sites.

But the problem is that many sites set unnecessary cookies like the analytics cookie or the cookie-consent banner cookie on all visitors. The result: nobody gets cached and your CDN is just an expensive proxy.

The solution is to separate the session cookie from the rest. In PHP, for example, change the session name to something like APPSESSID and in the CDN base the decision only on that name. The analytics cookie no longer affects caching.

Here's where people make a mistake: teams remove the cookie from the response so it gets cached, without knowing that cookie is needed for CSRF. The sign is that forms work for a few hours and then suddenly give 419 Page Expired. If you see this error, first look at the CDN cache settings, not the form code.

Cache invalidation after deployment

There are two schools of thought, and choosing between them depends on how you deploy.

MethodCostWhere it works
Purge by URLSlow, requires an exact list of pathsSmall sites with manual deployment
Versioning filenamesRequires changing the build pipelineAny site with CI/CD
Full purge (purge all)All traffic temporarily goes to originOnly in emergencies

I always choose versioning. I only do a full purge when a wrong response is being served to all users and must be stopped immediately. At that moment, your origin must be able to handle the full load; if it can't, the full purge itself creates a second incident.

If your infrastructure runs on managed Kubernetes, you can put filename versioning inside the same build pipeline and never purge manually again. For sites with heavy static traffic, separating files into object storage and serving them from the edge takes the pressure completely off the origin.

What you should never cache

  1. Any path that changes with the user session: shopping cart, user panel, checkout page.
  2. POST, PUT, and DELETE responses. These aren't cached by default, but some misconfigurations do it.
  3. Any response that has a Set-Cookie header.
  4. Error pages with a 5xx code. If the CDN caches a 502 for five minutes, the error is shown to everyone for five minutes.

Take the fourth one seriously. The default for some CDNs is to cache error responses briefly so the origin can breathe. This behavior helps during a DDoS attack but during a transient database error it enlarges the blast radius.

The order of operations in practice

First fix the origin headers, then turn on the CDN. If you do the opposite, you'll have to fight two variables at once and won't know which layer is producing the wrong response.

After turning it on, check a few key paths with curl -I:

curl -I https://example.com/account/
curl -I https://example.com/assets/app.4f9c2a.js
curl -I "https://example.com/product?utm_source=news"

In the output, look for X-Cache: HIT or CF-Cache-Status. The first path should be MISS or BYPASS. The second path should be HIT. The third path should return the same cache key as the path without the parameter.

If your site has variable traffic and you want to know how much the edge can handle, take a baseline of response times before any change to the CDN configuration. The guide on server monitoring and alerting covers exactly this, and without it, any change is a guess.

For sites whose incoming traffic is unpredictable, combining a CDN with ServerNet cloud servers lets you scale origin capacity independently of the edge. If needed, cloud infrastructure (IaaS) also gives you full control over the origin layer.

Frequently asked questions

Why do logged-in users appear as guests after enabling a CDN?

Because the response of a page that depends on the user session has been cached at the edge and the same response is given to the next user. The usual cause is the absence of the Cache-Control: private, no-store header in the origin response. Until you add this header, any setting in the CDN panel only covers part of the problem.

The permanent solution is to leave the caching decision to the origin and in the CDN just say "respect the origin header."

Should I keep the query string in the CDN cache key?

Only parameters that actually change the content. Tracking parameters like utm_source and gclid should be removed, otherwise every campaign link creates a fresh copy at the edge and the cache hit rate drops.

Parameters like lang or currency should stay, because the response really does differ.

After releasing a new version, how do I invalidate the cache?

The best method is to not need to at all: version your static filenames with a content hash so the new URL creates a new cache key. Keep the full purge for emergencies, because at that moment all traffic goes to the origin.

If you're forced to purge, first make sure the origin has the capacity to handle the full load.

Is caching dynamic HTML pages even worth it?

Yes, for pages that are the same for all users: homepage, product listing, articles. These usually have the most traffic and caching them greatly reduces database pressure.

But for any page that changes with the user cookie, the answer is clear: no. Draw the line based on the Vary header and the presence of Set-Cookie, not based on guesswork.

Before anything else, check three key paths of your site today with curl -I. If the user panel path returns HIT, the rest of the discussion is pointless.

ServerNet Support

ServerNet engineering & editorial team — specialists in infrastructure, networking and web hosting.

Cloud Infrastructure (IaaS)
Share:

Comments 0

No comments yet — be the first!

Leave a comment

Related service

Cloud Infrastructure (IaaS)

Servers, private networks, firewalls and storage — all API-driven and billed hourly. Infrastructure as code that scales with you.