
Every time a browser or a CDN edge server asks your site for a page, image, or script, your server sends back a set of instructions buried in the response headers. Those instructions decide whether the visitor’s next request gets served instantly from a local cache or has to travel all the way back to your origin. Get the headers right and you shave real time off your Core Web Vitals numbers, especially LCP. Get them wrong and you either serve stale content to everyone or force fresh downloads that a cache could have handled. Here is what each header actually promises, and what reasonable values look like for a WordPress site.
Cache-Control: the header that matters most
Cache-Control is the primary directive for both browsers and CDNs. It replaced the older Expires header and gives you finer control through a set of comma-separated directives.
max-age=Ntells a browser how many seconds it can reuse a cached copy before checking back in.s-maxage=Nis the same idea but specifically for shared caches like a CDN edge or a reverse proxy. It overrides max-age for those caches, which lets you tell Cloudflare to hold something longer or shorter than the visitor’s own browser does.publicmeans any cache, shared or private, is allowed to store the response.privatemeans only the end user’s browser should cache it, not a shared CDN or proxy. This matters for anything containing personalized data, like a logged-in admin bar or a cart total.no-cacheis a misleading name. It does not forbid caching; it means the cache must revalidate with the origin before serving the stored copy again, usually via ETag or Last-Modified.no-storeis the real “do not cache this at all” directive. Use it for anything sensitive, like checkout pages or admin screens.must-revalidatetells a cache it cannot serve a stale copy under any circumstance once max-age has expired, even if the origin is unreachable.immutabletells the browser the resource will never change at that URL, so don’t even bother checking. This is ideal for versioned static assets likestyle.css?ver=6.4.2, where a new version means a new URL entirely.
A typical WordPress page (an HTML response) benefits from a short public max-age plus a longer s-maxage at the CDN, so the edge can serve cached HTML to anonymous visitors while browsers still check back reasonably often. Static assets like images, fonts, and versioned CSS/JS can carry very long max-age values, often measured in months, because the filename or query string changes whenever the content does.
ETag and Last-Modified: validation, not just expiration
Cache-Control tells a cache how long it can skip asking you anything. ETag and Last-Modified handle what happens after that window closes.
An ETag is a fingerprint, usually a hash, representing the exact state of a resource. When a browser’s cached copy expires, it sends the stored ETag back in an If-None-Match header. If your server’s current ETag matches, it replies with a lightweight 304 Not Modified and no body at all, saving bandwidth and time. If the content changed, the ETag no longer matches and the server sends the full response with a new one.
Last-Modified works the same way but uses a timestamp instead of a fingerprint, checked via If-Modified-Since. It’s slightly less precise (timestamps have a one second resolution) but cheaper to compute, which is why many servers set both.
The practical takeaway: don’t disable ETags to save a few bytes of header space, as some minification guides suggest. A working validation header keeps repeat visits fast even after the cache lifetime expires, because a 304 response is nearly free compared to redownloading a full asset.
Vary: the header that quietly breaks caches
Vary tells a cache which request headers it needs to consider before reusing a stored response. Vary: Accept-Encoding is normal and harmless; it just means the cache should store separate gzip and non-gzip copies. But Vary: Cookie or Vary: User-Agent can quietly gut your cache hit rate, because nearly every visitor sends a slightly different cookie or user agent string, so the cache treats almost every request as unique. If your CDN or full-page cache hit rate looks unexpectedly low, checking the Vary header on your HTML responses is one of the first things worth doing.
Sane values for a WordPress site
Here is a reasonable baseline to aim for, whether you’re configuring this through a plugin, server config, or CDN dashboard:
- HTML pages (logged out):
Cache-Control: public, max-age=0, s-maxage=600, paired with cache-busting on publish so editors see changes immediately. - HTML pages (logged in, cart, checkout):
Cache-Control: private, no-store, so nothing personal ever gets cached upstream. - Images, fonts, versioned CSS/JS:
Cache-Control: public, max-age=31536000, immutable, since the filename changes on update. - REST API and AJAX endpoints: generally
no-storeor a very short max-age, since these often carry dynamic or personalized data.
These aren’t universal rules, and some sites have good reasons to deviate, but they’re a solid starting point.
How LiteSpeed Cache and Cloudflare split the work
On a LiteSpeed-based stack, the LiteSpeed Cache plugin handles full-page caching at the server level and sets sensible Cache-Control headers for HTML automatically, while Redis or Memcached handles object caching behind the scenes (a separate concern from HTTP headers, since object caching speeds up database queries rather than instructing browsers or CDNs). Cloudflare sits in front of that as the CDN edge, honoring s-maxage from your origin or applying its own edge cache rules, and adding the WAF, bot mitigation, and HTTP/3 on top. This is one area where a managed host like ServerBorn is genuinely useful to have handled for you, since getting LiteSpeed’s page cache and Cloudflare’s edge cache to agree on TTLs and purge together, without serving stale pages or fighting each other, is fiddly to configure by hand.
Checking headers yourself
You don’t need special tools. A quick curl command shows you exactly what your server is sending:
curl -I https://example.com/
curl -I https://example.com/wp-content/uploads/2024/01/photo.jpgLook for Cache-Control, ETag, Last-Modified, and Vary in the response. If you use wp-cli for other admin tasks, you can also confirm whether a caching plugin is active with a quick wp plugin list --status=active, since misconfigured or conflicting cache plugins are a common cause of inconsistent headers.
Takeaway
Cache-Control decides how long a cache can skip asking you anything. ETag and Last-Modified decide how cheaply it can check back in once that time is up. Vary decides whether the cache can share a stored response across visitors at all. Get those three right, alongside a no-store policy for anything personal, and both browsers and your CDN will do far more of the work for you, which shows up directly in faster repeat visits and better Core Web Vitals scores.