The WordPress Page Cache Header Lies. Here Is What To Check Instead..
A caching plugin can be active, configured and doing nothing. Two commands separate a cache that runs from one that only looks like it does.
Short answer: do not trust the plugin. Request the page normally, then request it again with a random query string so no cache can serve it, and compare the times. A working page cache shows a gap of several times. If the two are the same, nothing is being cached, whatever the dashboard says.
The wordpress page cache header is worth reading too, but it comes second, and on a lot of WordPress stacks it is not there at all.
The install that taught me to stop trusting settings
I set up a WordPress site this week with a caching plugin on it and went through the usual list. Everything checked out.
Then I looked for evidence the cache had ever stored anything.

you. The two red ones are what a visitor experiences. I never did work out the cause on this install, and I have left that in on purpose, because the lesson is not the cause.*
Every configuration check passed. The cache had never written a single file.
If I had reported on that site from the admin screens, I would have told a client their caching was fine. It was not fine. It was absent.
The test that cannot be fooled
Two requests. The second one carries a query string nothing has ever requested, so no cache can have a copy.
# as a normal visitor
curl -s -o /dev/null -w '%{time_starttransfer}\n' https://example.com/
# same page, cache bypassed
curl -s -o /dev/null -w '%{time_starttransfer}\n' "https://example.com/?n=$RANDOM"
Run each three times, because one sample is noise.

because the bypassed request has to boot WordPress, run the theme and hit the database, while the cached one reads a file off disk.*
What you are looking for is a gap. Several times over, not a few milliseconds.
Under roughly 1.5x on a real site and I stop believing there is a page cache in front of it.
Reading the WordPress page cache header, and knowing its limits
Headers are quicker than timing when they exist. The catch is that plenty of WordPress page caches, especially disk-based plugin caches, emit nothing to identify themselves.
curl -sI https://example.com/ | grep -iE 'x-cache|cf-cache-status|age|x-litespeed-cache'
A hit looks like x-cache: HIT, or cf-cache-status: HIT, or an age value above zero.
One header to be careful with: cache-control. Your own site writes that one. It says what you would like caches to do, and proves nothing about what any of them did.
I found a live example of that gap while measuring for the companion piece on TTFB. Our own marketing site sends a one-year s-maxage and Cloudflare still answers DYNAMIC. The instruction is being sent. It is not being followed.

the bottom half can be true on a site that caches nothing at all, which is exactly the site I had in front of me.*
The one that costs real money
There is a second failure that runs the other way, and it is worse.
A cache that is working, on a page that should never be cached.
Cart, checkout and account pages have to be excluded, or one customer sees another customer’s basket. Most cache plugins exclude WooCommerce pages by default, and most of the time that default survives.
It does not always survive a hosting migration, a new CDN rule, or somebody adding a page-rule that was meant to fix something else.
Test it by requesting a cart URL with and without a login cookie, and confirming you never get a cached response on either.
Why this ends up on the never-done list
The check takes a minute. It needs redoing after every hosting change, every CDN change and every cache plugin update, on every site you look after.
So it gets done once and then trusted forever, which is the same trap as the scheduled jobs nobody notices have stopped and the backup file left in the web root.
Protuno’s free audit reads the caching headers from the domain today, so you can see at a glance which sites in a portfolio are announcing a cache and which are not. Being straight about the rest: Dash, the Performance agent that would re-run the timing comparison on a schedule, is built and named but not live yet.
The two curl lines above are the whole method in the meantime. Run them on the client site you are most confident about. That is the one worth checking.
Comments