WordPress 6.8+ kills your edge cache. Edgenote brings it back.
A surgical Cache-Control header helper for Cloudflare and other edge CDNs.
WordPress 6.8 changed wp_get_nocache_headers() so it now always emits:
Cache-Control: no-cache, must-revalidate, max-age=0, no-store, private
— regardless of whether the request is anonymous or authenticated. Cloudflare (and any well-behaved shared cache) refuses to cache anything containing private or no-store. The result: every visitor hits the origin every time, TTFB craters, and the "edge cache" is decorative.
The header gets emitted from at least three different places in core (nocache_headers filter, wp_headers filter, and direct header() calls in send_headers), so a half-fix that only hooks one of them gets quietly clobbered by the other two.
Edgenote hooks all three insertion points and runs at low priority so it always wins:
nocache_headersfilter (priority 99) — replacesCache-Control, dropsExpires/Pragma, removesCDN-Cache-Control.wp_headersfilter (priority 99) — replacesCache-Control, setsVary: Accept-Encoding, Cookie, removesCDN-Cache-Control.send_headersaction (priority 999) — explicitheader(..., true)call as the final word, plusheader_remove('CDN-Cache-Control')for the rogue header WP 6.8 sometimes emits direct.
Only fires on requests that are actually safe to cache:
- Anonymous (no logged-in user, no
wordpress_logged_in_*/comment_*/wp-postpass_*cookies) GETorHEADonly- Not admin, AJAX, REST, search, feed, 404, or preview
- Path doesn't match the bypass list (
/wp-admin,/wp-login.php,/feed)
Emitted Cache-Control on a cacheable request:
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400
Vary: Accept-Encoding, Cookie
max-age=0 means the browser never caches (so a logged-in admin who returns to a page after login still gets fresh content). s-maxage means the edge caches for 5 minutes by default. stale-while-revalidate lets the edge serve stale while refreshing.
- Drop the plugin into
wp-content/plugins/edgenote/and activate (or zip-install). - Defaults are sane: 5-minute edge TTL, 1-day stale-while-revalidate.
- Settings → Edgenote to tune. Click Test headers to verify what visitors see.
Settings → Edgenote (option key edgenote_settings):
| Field | Default | Purpose |
|---|---|---|
Edge cache TTL (s-maxage) |
300 |
Seconds shared caches may serve a cached copy |
| Stale-while-revalidate | 86400 |
Seconds shared caches may serve stale during background refresh |
| Bypass cookies | wordpress_logged_in_, comment_, wp-postpass_ |
Cookie name prefixes that mark a request as authenticated |
| Bypass paths | /wp-admin, /wp-login.php, /feed |
URL fragments that always skip the override |
Cookie and path lists are one prefix per line; substring match against REQUEST_URI for paths and against the cookie name for cookies.
Two ways to make Cloudflare honor the Cache-Control we emit:
- Caching → Configuration → Respect Existing Headers (recommended). Cloudflare obeys whatever
s-maxageEdgenote sets. - Page Rule with Edge Cache TTL set explicitly. Bypasses Cache-Control entirely; Edgenote still helps because Cloudflare's "cache by default" rules require Cache-Control to be public.
After setup, hit a public URL with curl -I https://yoursite.com/ and look for cf-cache-status: HIT after the first request.
wp boot
│
▼
┌──────────────────────────┐
│ CacheableRequest │ anonymous? GET/HEAD?
│ - method check │ not admin/REST/AJAX/preview/search/feed?
│ - cookie bypass list │ no bypass-cookie? no bypass-path?
│ - path bypass list │
└────────────┬─────────────┘
│ yes
▼
┌──────────────────────────────────────────────┐
│ HeaderOverride │
│ ├ nocache_headers filter (priority 99) │
│ ├ wp_headers filter (priority 99) │
│ └ send_headers action (priority 999) │
│ │
│ sets: │
│ Cache-Control: public, max-age=0, │
│ s-maxage=N, stale-while-revalidate=M │
│ Vary: Accept-Encoding, Cookie │
│ strips: │
│ Expires, Pragma, CDN-Cache-Control │
└──────────────────────────────────────────────┘
- Anonymous-only. Logged-in users always bypass the override; per-user edge caching is out of scope.
- No per-route TTL. A single
s-maxagecovers every cacheable request. Use a Cloudflare Page Rule or aCache Ruleif you need per-path control. - No purge integration. Edgenote sets headers; it does not call Cloudflare's purge API on
save_post. Pair with a purge plugin (e.g.cloudflare,wp-rocket) if you need invalidation on content updates. - Substring path match. A bypass entry
/feedalso bypasses/category/feedback/. Tune the bypass list to match the literal URL prefix.
MIT.