The product page still shows the old price after a flash sale — support blames "the CDN." In reality, PHP sends Cache-Control: public, max-age=3600 on catalog HTML, Cloudflare honours that directive, and only the cart API returns no-store. JavaScript assets stay fresh thanks to hashed filenames; catalog HTML stays frozen for a full hour. Nobody documented which layer caches which resource.
HTTP cache headers are the contract between your origin, CDN, and browser. When misaligned, you pay several times over: latency for users, origin overload, or stale content that erodes trust. A CDN switched on without a coherent policy adds a network hop without delivering the benefit.
What follows sets out a resource-by-resource matrix — not a "cache enabled" slider in the hosting panel.
Cache-Control: the directives that actually matter
Cache-Control is the directive browsers and CDNs read most often. Each keyword has a precise effect:
| Directive | Effect |
|---|---|
max-age=N | Resource considered fresh for N seconds in the browser |
s-maxage=N | Same logic, but for shared caches (CDN) |
no-store | No storage — sensitive data, account pages |
no-cache | Storage allowed, revalidation required before use |
immutable | Fingerprinted asset — no pointless revalidation |
stale-while-revalidate | Serve stale version while refreshing asynchronously |
For dynamic HTML (catalog, articles, personalised pages), prefer private, no-cache or a short max-age with revalidation. For static assets with a hash in the name (/app.abc123.js), public, max-age=31536000, immutable is the expected standard.
A CDN does not guess your intent — it obeys headers, unless an explicit override rule exists in the panel.
On shared hosting or a VPS with bundled CDN (OVH, Infomaniak, etc.), check that panel rules do not contradict what your application sends. Conflicts between nginx, PHP, and the CDN are among the most common causes of stale content.
CDN, origin, and the Vary header trap
The Vary header tells the cache which request dimensions affect the response. Two cases come up constantly:
Vary: Accept-Encoding — essential if you serve Brotli and gzip depending on the client. Without Vary, a CDN may cache the gzip version and serve it to a client expecting Brotli.
Vary: Accept — required if you negotiate image format (WebP, AVIF) via the Accept header. Without Vary, the CDN may serve AVIF to a browser that does not support it. For the full strategy, see WebP and AVIF.
Configure the CDN cache key: include encoding, exclude session cookies on public assets. A Set-Cookie on a CSS file can prevent edge caching with some providers.
ETag, 304 responses, and origin load
Conditional revalidation avoids re-downloading unchanged files. The client sends If-None-Match with the received ETag; the server responds 304 with no body if content is identical.
On a multi-node cluster, a per-machine ETag (inode, timestamp) becomes inconsistent: the client gets 200 instead of 304, and the origin picks up load again. Prefer a stable content hash, or delegate caching to the CDN with long TTLs on fingerprinted assets.
For user JSON APIs (/api/me, /api/orders), no-store or a very short per-user cache remains the norm. Shared CDN caching on these routes exposes private data.
Strategy by resource type
Each file type deserves its own policy — documented, tested, shared with the team:
| Resource | Recommended policy |
|---|---|
| Hashed CSS/JS | 1 year, immutable |
| Static images | 7–30 days + targeted purge on deploy |
| Public HTML | Short max-age + stale-while-revalidate, or no-cache |
| User API | private, no-store |
| Feeds, sitemaps | Short max-age or ETag revalidation |
Under WordPress, plugins modify headers without coordination — audit the wp_headers filter and compare what nginx, PHP-FPM, and the CDN return. A page-cache plugin can conflict with a minification plugin that changes filenames.
For deployment and invalidation, cross-read with HTTP/2 multiplexing: fewer connections do not compensate for poorly cached assets.
Purge, deploy, and classic mistakes
Three errors show up in every audit:
- Forgetting CDN purge after non-fingerprinted HTML deploy — visitors see the old version for hours.
Cache-Control: max-age=0withoutno-store— variable CDN behaviour; some still store the response.- Cookies on assets —
Set-Cookieon a static file can block edge caching.
Test systematically with curl -I from the CDN edge and from the origin. Headers should match, or the difference should be documented (TLS termination at CDN, compression, etc.).
Compare offers with bundled CDN via our directory and compare tool — some hosts impose default rules you need to know before deploying.
The peak: CDN on, nothing cacheable
Here is what the "CDN enabled" checkbox in the panel does not tell you.
Cooperating three layers means each file type has its policy — not a global "cache on" setting.
Decide and move forward without blind spots
In half a day, you can put caching back on solid ground:
- Draw a matrix resource → directive (
Cache-Control,Vary, duration) and share it with the team. - Fingerprint static assets (hash in filename) to allow long TTLs without blind purge.
- Check Vary headers if you negotiate compression or image format.
- Test with
curl -Ifrom edge and origin — document expected differences. - Plan targeted CDN purge on each HTML deploy, never "purge everything" without a rollback queue.
- Compare CDN hosts via the directory if you are migrating or renegotiating your stack.
For a quick audit, pick one HTML URL and one JS file — headers must be consistent across all three layers. If you already host with a European provider, check its profile on Hébergeurs.eu for default CDN rules.
Frequently asked questions
public vs private vs no-store?
public lets the CDN cache the response for all visitors. private limits storage to the user's browser — suited to account pages. no-store forbids all storage, including in memory, for sensitive data. no-cache allows storage but requires revalidation before use.
max-age and s-maxage?
max-age applies to the browser; s-maxage to shared caches like the CDN. On a versioned asset (/app.abc123.js), a long max-age with immutable avoids pointless requests. Dynamic HTML deserves short durations or systematic revalidation.
How to invalidate after deploy?
Fingerprint files (hash in name), purge the CDN selectively on affected URLs, or add a version parameter. Avoid full production purge without a rollback plan — it can saturate the origin if all traffic becomes cache misses.
ETag or Last-Modified?
ETag enables precise conditional revalidation (304 with no body). Last-Modified works too, but content-hash ETag is more reliable in a cluster. Watch for per-machine ETags — they make revalidation ineffective.
Effective caching is read in headers — not the CDN checkbox in the hosting panel.