Relayer
DocsOperationscdn-cache

CDN Caching#

The Cache-Control you declare in HTTP Caching & ETag also works directly on a CDN's (Cloudflare etc.) edge cache. For a read-heavy site, you can hit on the edge and reduce reaching the origin (PHP / remote DB) to nearly zero. However, putting HTML on the CDN requires CDN-side configuration, and there is a Relayer-specific trap where a session cookie causes a full BYPASS, so this focuses on those two points.

Separating browser and CDN (max-age vs s-maxage)#

Cache's maxAge is the browser's freshness, sMaxAge is the shared (CDN) cache's freshness. A browser cache cannot be purged (once served, a fix never arrives), whereas a CDN can be purged, so setting them asymmetrically is the standard practice.

// short for the browser, long for the CDN; edits reflect immediately via a CDN purge
new Cache(public: true, maxAge: 300, sMaxAge: 2592000, etag: $etag);
// → Cache-Control: public, max-age=300, s-maxage=2592000

Centralize the shared policy in one place (e.g. App\PageCache::timed()) and keep the TTL as the only knob there to make operations easy.

Making Cloudflare cache HTML#

Cloudflare by default caches only static assets (extension-based) and treats HTML / JSON as "dynamic," passing them through. Adding s-maxage alone is not enough. Create a Cache Rule for the target paths (the equivalent of the old Page Rule's "Cache Everything" = Eligible for cache) and set the Edge TTL to respect origin (Use origin cache-control). Now Cloudflare adopts s-maxage and caches at the edge for the specified number of seconds.

  • Target every read path (e.g. /, /docs/*, /search, /api/*). If the rule's path scope covers only part of them, the pages outside it stay MISS forever and are never cached.
  • Purge the zone on every deploy (e.g. have CI hit Cloudflare API's purge_everything). Stale HTML left at the edge keeps serving the previous deploy's forms and rosanna-defer.js, so form submissions and defer fetches disagree with the new server.

Cloudflare unconditionally bypasses caching for any response that includes a Set-Cookie (cf-cache-status: BYPASS). Here is the Relayer-specific pitfall.

Relayer's page rendering (Rosanna) uses no session and no state. session_start() runs only when a page touches the session, such as authentication (Authenticator) or a server action's CSRF token (_rosanna_csrf). A page that does not touch it emits no Set-Cookie: PHPSESSID, so it is edge-cacheable. Only pages that do emit Set-Cookie and are sent with Cache-Control: private, no-store, so those are BYPASSed as intended.

Points:

  • Do not block this by setting session.use_cookies=0 on the app side. The CDN may go through, but it silently breaks pages that later use auth / CSRF. The correct fix is "leave it to the framework."
  • Separately, keep session.cache_limiter empty. When a session starts, PHP injects Cache-Control: no-store, no-cache, must-revalidate, overriding the per-page cache policy.
// public/index.php, before Relayer::boot()
\ini_set('session.cache_limiter', '');

Verifying it works#

Measure the response headers for real.

curl -sI https://example.com/docs/some-page | \
  grep -iE 'cache-control|cf-cache-status|age|set-cookie'
  • There is no set-cookie: (if present, it is the cause of BYPASS).
  • cf-cache-status: is MISS → HIT on re-access, with age: increasing.
  • cache-control: carries the intended s-maxage.

DYNAMIC means the Cache Rule is not set (HTML not targeted), BYPASS means cache excluded due to Set-Cookie etc. Right after purging the zone the edge is unpopulated, so MISS continues for a while — wait a few tens of seconds and check again.

Reflecting edits and purging#

Within max-age / s-maxage the origin is not queried, so even after you update content the CDN stays stale for at most s-maxage seconds. If you have CI that purges the zone on deploy, deploy reflection is immediate, but in operations that update only content without a deploy (editing the external DB directly, etc.) that update does not appear on the CDN until you purge. If you use a long s-maxage, always build a CDN purge into the content-update path too.

Related: HTTP Caching & ETag · Deployment (Dockerfile)

Last revised — 2026-11-04
Revisions (3)
  • v1.1.0: usePHP→Rosanna(rosanna-defer)に合わせて更新
  • Sidebar reorg: order shift for the new Development category
  • Created