Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

What Is a CDN Cache Key, and Why Are Your Users Seeing Stale Pages?

About Post

You deployed the fix twenty minutes ago. Your laptop shows the new page. Your colleague's phone shows the old one. A customer sends a screenshot of a version you replaced last week. Someone suggests "clear your cache", and it works for exactly one person.

And then there's the scarier cousin of that bug: a user who opens the site and sees a page meant for someone else, or a page in the wrong language.

Both problems usually come from the same small concept that most developers never look at directly: the CDN cache key. Let's open it up.

A cache key is the CDN's filing system

A CDN keeps copies of your responses on servers close to your users. When a request arrives, it has to answer one question: do I already have this exact response?

To answer it, it builds a cache key from parts of the request. A typical default is roughly:

https + www.example.com + /units + ?page=2

Scheme, host, path and query string. If the key matches a stored copy that's still fresh, the CDN serves it without asking your server. The exact defaults differ between providers, and most let you configure them (CloudFront, for example, uses cache policies that decide which headers, cookies and query strings are part of the key).

The crucial part is what's not in the key. By default that's usually cookies, most headers, and anything about who the user is. The CDN treats all requests with the same key as the same request.

Think of a library that files books only by title. Two different books with the same title? One of them will be handed to the wrong person.

Mystery 1: the page that belonged to someone else

Your server renders a page differently depending on something outside the key: the session cookie, the Accept-Language header, the device type. The CDN doesn't know that. It stores the first version it sees, and serves it to everyone with the same URL.

That's how one user's dashboard, greeting or language ends up shown to others. It's not a hacker; it's a filing mistake. And when the page contains personal data, it's a serious one.

The fixes, from most to least important:

  • Mark personal responses as uncacheable by shared caches: Cache-Control: private, no-store on account pages and authenticated API responses. Don't rely on your CDN's defaults to guess.
  • Put the thing that varies in the URL (/en/, /ar/) rather than in a header, so it's naturally part of the key.
  • Or add it to the key explicitly, using the Vary header or your CDN's cache key settings.

The Vary header, and why it's a trap

Vary tells caches "this response depends on these request headers, so include them in the key". Vary: Accept-Encoding is normal and harmless: gzip and Brotli versions are stored separately.

But Vary: Cookie or Vary: User-Agent is a different story. Every user has different cookies; there are countless user agent strings. Your one cached page becomes thousands of tiny caches, each used once, and your hit rate collapses. CDNs also differ in how fully they honour Vary, so check your provider's docs rather than assuming.

Mystery 2: query strings that split (or merge) your cache

Query strings cause problems in both directions.

Too much in the key. A marketing campaign adds ?utm_source=newsletter&utm_campaign=june to your links. To the CDN, every combination is a different page, so every visitor from the campaign misses the cache and hits your server, exactly when traffic is highest. Parameter order matters too: ?a=1&b=2 and ?b=2&a=1 may be two keys unless your CDN normalises them.

Too little in the key. Someone "fixes" that by telling the CDN to ignore query strings. Now /units?page=2 returns the cached page 1. Search results all look identical.

The right setting is an allowlist: include the parameters that change the content (page, sort, q), ignore the ones that don't (tracking parameters).

Mystery 3: the fix that didn't arrive

Now the stale page. How long a copy stays fresh comes from your Cache-Control header (or the CDN's default TTL if you don't send one). Two directives matter most:

  • max-age: how long any cache, including the user's browser, may reuse the response.
  • s-maxage: overrides max-age for shared caches like CDNs only.

Here's the part that catches people: purging the CDN doesn't touch browsers. If you sent HTML with max-age=86400, every browser that loaded it may keep showing the old page for a day, whatever you do at the CDN. You can't call those copies back.

A setup that avoids most of this pain:

# Fingerprinted assets (app.3f9a2b.js): cache "forever"
Cache-Control: public, max-age=31536000, immutable

# HTML: browsers revalidate, the CDN keeps it briefly
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=60

# Account pages and personal API responses: never shared
Cache-Control: private, no-store

Build tools like Vite already put a content hash in asset file names, so a new deploy means new URLs and new cache keys. Nothing needs purging. The HTML that references them stays short-lived, so users pick up new asset URLs quickly.

In Laravel, the built-in cache.headers middleware sets these for a group of routes:

Route::middleware('cache.headers:public;max_age=0;s_maxage=300;etag')
    ->group(function () {
        Route::get('/units', [UnitController::class, 'index']);
    });

One Laravel gotcha: routes in the web middleware group start a session and send a Set-Cookie header on every response. Many CDNs won't cache a response that sets a cookie, and a CDN configured to cache it anyway may hand one visitor's session cookie to the next. Public, cacheable pages should not set cookies at all.

The rule: cache by URL design, not by hope. Versioned file names for assets, short TTLs for HTML, private, no-store for anything personal, and a query string allowlist. Then purging becomes rare instead of routine.

When you do need to purge

  • By URL: precise, but remember every variant of the key (with and without query strings, both hostnames).
  • By prefix or tag: some CDNs let you tag responses (for example, every page that shows unit 42) and purge the tag. Very useful for content-heavy sites.
  • Everything: the big red button. It works, but every request suddenly goes to your origin at once. Avoid it during peak traffic.

How to debug it in two minutes

Look at the response headers with curl -I or the browser's network tab. The Age header tells you how many seconds the copy has been in a cache. Most CDNs add a hit/miss header too (CF-Cache-Status on Cloudflare, X-Cache on CloudFront). If Age is large and the status is a hit, you're looking at a cached copy, and now you know which layer to blame.

Recap

  • The cache key decides what counts as "the same page". Know what's in yours.
  • Anything that changes the response but isn't in the key is a bug waiting to happen.
  • Be careful with Vary; allowlist query strings.
  • Purges don't reach browsers; versioned URLs do.

What's the strangest caching bug you've chased? I have a feeling most of them end with the words "it was the query string".

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close