We have written about CDNs and about Core Web Vitals, but not about caching, and that is an omission. Because of the three, caching is the one that gives the biggest improvement for the least work, and it is also the one most often configured wrongly.

Let us clear up the difference first, because people mix them up constantly. A CDN reduces distance. Caching reduces work. The first brings the file closer to the visitor. The second avoids rebuilding the file from scratch. Two different problems needing two different solutions, and you usually want both.

The one directive that does most of it

There are not dozens of settings. There is mainly one header, Cache-Control, and two values you need to know.

Before those, some housekeeping. You will find plenty of guides citing RFC 7234 for caching. It has been obsoleted. The current standard is RFC 9111, “HTTP Caching”, from June 2022. I mention it because, like the Search Console parameters tool, it is a good age test: if what you are reading cites 7234, it was written at least four years ago.

The only two things to remember

For static files with unique names, the ones your build tool emits as style.a7f3d2.css and renames every time the content changes:

Cache-Control: max-age=31536000, immutable

That is one year in seconds, or as web.dev puts it, “effectively equal to forever”. And it is risk-free precisely because the name changes when the content changes. You never need to “clear” anything: the new file has a different name, so the browser fetches it anyway.

For HTML pages, exactly the opposite:

Cache-Control: max-age=0, must-revalidate, public

Keep a copy, but ask me every time before using it. Because your page URL does not change its name when you edit a paragraph, so there is no other way for the browser to learn that something happened.

Those two are ninety per cent of the job. The rest is detail.

The name that misleads everyone

no-cache does not mean “do not store”. The Chrome team says so themselves, with unusual candour for documentation: “no-cache is a confusing name, because it could be interpreted as never cache this file, even though that’s not the case”.

It means: store it, but ask before you use it. It is essentially the same as the max-age=0, must-revalidate above.

The one that means “do not store” is no-store, and it is far heavier. You use it on pages with personal data, on dashboards behind a login, on anything that should not remain on the user’s machine. Not on your home page.

Confusing the two costs you in both directions. Some people put no-store everywhere and kill every improvement, others put no-cache on files that could have sat there for a year.

How many get it right? The honest answer

I would like to give you a percentage, and I looked. There is not one.

The Web Almanac, the largest annual measurement of the web, has no caching chapter in either the 2024 or the 2025 edition. The last one was around 2021. You will find texts saying “only 38% configure headers correctly”. I found no such figure anywhere.

Two related numbers do exist, from other chapters of the 2024 measurement: 21% of sites use Cache-Control: no-store, and 25% of mobile sites use no caching at all. The first is interesting because no-store has a side effect people forget: it stops the page entering the browser’s back/forward cache, so the back button reloads everything.

Two incidents from our own site

Since we have been theoretical so far, let us get specific with our own mistakes.

The first was ours, and we fixed it. mslogic.gr was sending max-age=3600 on HTML pages, meaning one hour. That sounds reasonable until you think about what it means: you correct a piece of text and a visitor who came by forty minutes ago still sees the old one. We brought it down to zero with mandatory revalidation, which is exactly the recommendation above. Static files stayed at one year, because they are fingerprinted.

The second was more insidious, and it fooled us. After an upload, the page was still showing the previous version even though the header said zero. At the same moment, with a parameter appended to the URL, the new one came back instantly. So the setting was not at fault: there was an intermediate copy on the server’s proxy that had not refreshed.

I write it up because it has two practical consequences that may apply to you. When you check after an upload, add a ?v=1 to the URL, otherwise you may be looking at a copy and concluding something went wrong. And if an automated check runs immediately afterwards, it may report something missing that is actually there.

The trap that wipes out your security headers

And a warning for anyone thinking of configuring this in nginx themselves, because we nearly walked into it.

The obvious move is to create a location block for HTML files and put an add_header for caching inside it. Do not do it that way. In nginx, once a location defines even one add_header, it loses every add_header it inherits from the level above.

You see what that means. HTML pages, which are precisely the ones that need the Content-Security-Policy and the other security headers, would be left without them. You would gain a few seconds and lose the armour, with no warning anywhere.

The correct shape is the reverse: short caching as the general rule at server level, with a long-lived exception only for static files.

We did not end up touching it at all, because for Cache-Control it turned out .htaccess was responsible rather than nginx. But we found that by looking at the headers that actually arrive, not by reading the configuration files.

And the most modern piece

There is a third option few people know: stale-while-revalidate.

It tells the browser: show the old copy immediately, and go and fetch the new one in the background. The visitor sees something almost instantly, and the next load is up to date.

It is excellent for content that changes, but not every minute. A product listing, a category page, a blog index. Not for prices changing as we speak, and not for stock levels.

What to take away

If you have a static site, the odds are you are only missing the right value on the two things above, and it is half an hour’s work. If you have WordPress, the subject is bigger and has more layers, because there every page is built from scratch on every visit unless somebody stops it.

What both cases share is that it cannot be bought. There is no plugin that gets it right without somebody knowing what outcome they want. And the worst mistake is not having no caching, it is having the wrong caching: serving stale prices to a customer, or keeping data on their machine that should never have stayed there.

Which brings us back to the distinction at the start. A CDN brings the file closer, caching builds it fewer times. If you want to see what your server is sending today and what it should be, get in touch.