Skip to content

Simple guide · Overview

Page status: Active

Cloudflare Cache Observability

Issue #91 adds a dedicated cache observability layer for NutsNews public routes.

Visual overview

Primary diagram

System map

Issue #91 adds a dedicated cache observability layer for NutsNews public routes.

Render the repository-owned system map when you need it.

Diagram is not rendered yet.

View as text
Cloudflare Cache Observability

Issue #91 adds a dedicated cache observability layer for NutsNews public routes.

flowchart TB
  accTitle: Cloudflare Cache Observability
  accDescr {
    Issue #91 adds a dedicated cache observability layer for NutsNews public routes.
  }
  A["Cloudflare Cache Observability"] --> B["CLOUDFLARE_CACHE_OBSERVABILITY.md"]
  B --> C["Auto-generated placeholder diagram"]

Cloudflare Cache Observability

Fullscreen diagram view.

Issue #91 adds a dedicated cache observability layer for NutsNews public routes.

Caching protects Supabase, the VPS primary origin, the Vercel fallback target, and the reader experience. The goal is to catch accidental cache regressions such as no-store headers, missing CDN headers, unexpected policy markers, or /api/articles falling out of the cacheable path.


The cache observability checker compares expected and actual response headers for these public routes:

RouteExpected policyWhy it matters
/public-home-cache-3600sHomepage traffic should be CDN-cacheable.
/articles/<id>public-article-cache-3600sArticle detail pages should not hit origin for every reader.
/api/articles?limit=1public-api-cache-3600sHighest-priority route because API regressions can increase Supabase pressure.
/sitemap.xmlpublic-sitemap-cache-3600sSearch crawler route should be cached longer than reader pages.
/robots.txtpublic-robots-cache-3600sSearch crawler policy route should be cached longer than reader pages.
/icon.pngpublic-static-asset-cache-immutableStatic assets should be immutable and edge-cacheable.
/apple-icon.pngpublic-static-asset-cache-immutableStatic app icons should be immutable and edge-cacheable.

The source of truth is:

web/cache-observability.config.json

Public anonymous reader pages and feed APIs use a conservative browser cache header and a longer CDN-specific cache header:

cache-control: public, max-age=0, must-revalidate
cdn-cache-control: public, s-maxage=3600, stale-while-revalidate=86400
cloudflare-cdn-cache-control: public, s-maxage=3600, stale-while-revalidate=86400
vercel-cdn-cache-control: public, s-maxage=3600, stale-while-revalidate=86400

The one-hour CDN cache window is configurable in the web app with:

NUTSNEWS_PUBLIC_CDN_S_MAXAGE_SECONDS=3600
NUTSNEWS_PUBLIC_CDN_STALE_WHILE_REVALIDATE_SECONDS=86400

Only anonymous/public content should use these headers. Admin pages, auth routes, contact form submissions, monitoring routes, preview-only/private routes, POST or mutation routes, and owner-only routes must keep no-store CDN headers.


Validate the route configuration without making network requests:

Terminal window
cd web
npm run audit:cache:config

Run a live check against production:

Terminal window
cd web
npm run audit:cache -- --url https://www.nutsnews.com

Spot-check the most important public routes with curl:

Terminal window
curl -I https://nutsnews.com/
curl -I "https://nutsnews.com/api/articles?limit=5"
curl -I "https://nutsnews.com/articles/<article-id>"

Look for cache-control: public, max-age=0, must-revalidate and at least one visible CDN cache-control header with s-maxage=3600 and stale-while-revalidate=86400. Cloudflare, VPS-primary origin handling, and Vercel fallback handling may consume targeted CDN headers before the final browser response, so the cache observability report is the stronger end-to-end check.

Run a live check against a Vercel preview URL:

Terminal window
cd web
npm run audit:cache -- --url https://<preview-url>

If article discovery cannot find an article, pass one manually:

Terminal window
cd web
npm run audit:cache -- --url https://www.nutsnews.com --article-path /articles/<article-id>

The report is written to:

web/reports/cache-observability/cache-observability.md
web/reports/cache-observability/cache-observability.json

Workflow:

.github/workflows/cloudflare-cache-observability.yml

It has two jobs:

JobWhen it runsWhat it does
Cache config regression checkPull requests and pushes to mainValidates the cache route policy config and uploads a report artifact.
Live cache policy and alert checkScheduled and manual workflow dispatchChecks live Cloudflare/VPS-primary response headers and fails when cache policy regresses.

The live job runs every 6 hours by default.

A failed scheduled run is the alert. GitHub will surface the failing workflow, annotations, and the uploaded report artifact.

After the VPS-primary cutover, the repository variables should keep the live job pointed at the public production URL:

NUTSNEWS_CACHE_OBSERVABILITY_URL=https://www.nutsnews.com
NUTSNEWS_PRIMARY_PRODUCTION_URL=https://www.nutsnews.com/
NUTSNEWS_VPS_PRODUCTION_DIRECT_URL=https://vps.nutsnews.com/
NUTSNEWS_VERCEL_SECONDARY_PRODUCTION_URLS=https://nutsnews.vercel.app/

Do not point the scheduled cache observability job at the direct VPS hostname or the Vercel secondary hostname during normal operations. Those targets are useful for diagnosis, but the alert should represent the reader-visible Cloudflare production entrypoint.


Protected admin route:

/admin/cache

The dashboard shows:

  • Overall pass/warn/fail status
  • Expected vs actual X-NutsNews-Cache-Policy
  • Cache-Control, CDN-Cache-Control, Cloudflare-CDN-Cache-Control, and Vercel-CDN-Cache-Control
  • cf-cache-status when the checked URL is behind Cloudflare
  • Article page discovery status
  • Per-route findings

By default the admin page checks the current request host. On a Vercel preview deployment, /admin/cache checks that preview URL. In production, it checks the production host.

Optional environment overrides:

NUTSNEWS_CACHE_OBSERVABILITY_URL=https://www.nutsnews.com
NUTSNEWS_PRIMARY_PRODUCTION_URL=https://www.nutsnews.com
NUTSNEWS_CACHE_ARTICLE_PATH=/articles/<article-id>

Use the URL override when testing a preview deployment, a Vercel fallback drill, or when article discovery needs a known article URL. If NUTSNEWS_CACHE_OBSERVABILITY_URL is unset, the scheduled workflow falls back to NUTSNEWS_PRIMARY_PRODUCTION_URL and then https://www.nutsnews.com.


Reader-facing public routes should include these headers:

Cache-Control
CDN-Cache-Control
Cloudflare-CDN-Cache-Control
Vercel-CDN-Cache-Control
X-NutsNews-Cache-Policy

/api/articles should also include:

X-NutsNews-Article-Data-Source
X-NutsNews-Feed-Snapshot

These API headers show whether the request is served by the Supabase public feed snapshot, the articles fallback, or the Cloudflare edge snapshot fallback.


The live check fails when:

  • A required route returns an unexpected HTTP status.
  • A required cache header is missing.
  • X-NutsNews-Cache-Policy does not match the expected policy.
  • Cache-Control contains no-store or private on a public route.
  • Required CDN cache hints such as s-maxage=3600, stale-while-revalidate=86400, or immutable are missing.
  • /api/articles reports Cloudflare BYPASS or DYNAMIC for all observed samples.

Warnings are used when cf-cache-status is not present. That is normal on local checks and Vercel preview checks before Cloudflare.


After Vercel creates a preview URL for the PR, run:

Terminal window
cd web
npm run audit:cache -- --url https://<preview-url>

Look for:

  • Status: pass or warnings only for missing cf-cache-status.
  • Homepage policy: public-home-cache-3600s.
  • Articles API policy: public-api-cache-3600s.
  • Sitemap policy: public-sitemap-cache-3600s.
  • Robots policy: public-robots-cache-3600s.
  • Static icon policy: public-static-asset-cache-immutable.
  • No public route with Cache-Control: no-store.

On production behind Cloudflare, also look for cf-cache-status samples. A first request can be MISS; repeated requests should eventually show HIT for cacheable routes that Cloudflare can cache. Dynamic routes may legitimately report DYNAMIC; treat the workflow report and route policy markers as the source of truth.

Production deploys do not automatically purge the Cloudflare zone. The manual operator recovery path is Manual Cloudflare Production Cache Purge:

.github/workflows/cloudflare-production-cache-purge.yml
confirmation=purge-production-cache
dry_run=false

Use it after a production release only when stale edge content or a cache/header policy change needs immediate correction. Then rerun the curl checks and the cache observability audit against https://www.nutsnews.com.

Post-cutover evidence from issue #398:

CheckResult
Manual purgeramideltoro/nutsnews run 29887566515 passed
Live cache observabilityramideltoro/nutsnews run 29887610632 passed
Previously stale sitemap shardhttps://www.nutsnews.com/articles/sitemap/0.xml returned HTTP 200 after purge
Source/build under test521fcababfbaff54785c6b5cf5995dc7f9cf62a1, build 29885890520-1

Check:

web/app/api/articles/route.ts
web/lib/cacheHeaders.ts
web/middleware.ts
web/next.config.ts

The route should return ARTICLE_API_CACHE_HEADERS on successful responses.

Check web/middleware.ts and make sure the route is not accidentally included in isOperationalNoStoreRoute.

Check the long-cache rules in:

web/middleware.ts
web/next.config.ts

They should use a 3600-second CDN policy.

Static assets are missing Vercel CDN headers

Section titled “Static assets are missing Vercel CDN headers”

Check the static asset entries in:

web/next.config.ts

Static assets should send Cache-Control, CDN-Cache-Control, Cloudflare-CDN-Cache-Control, and Vercel-CDN-Cache-Control with public, max-age=31536000, immutable.

Cloudflare-CDN-Cache-Control and Vercel-CDN-Cache-Control are origin/CDN control headers. They may be consumed or hidden by the CDN layer and should not be required as visible browser response headers. The dashboard treats missing targeted CDN control headers as warnings when the visible cache policy marker and shared CDN/browser cache headers still show the route is cacheable.

For article detail pages, Cloudflare HIT is treated as the edge-cache source of truth. A browser-visible private or no-store header on an article page is still surfaced as a warning so it can be reviewed separately.

A visible Cache-Control header that includes s-maxage also counts as CDN cache evidence. Some platforms consume or hide targeted CDN headers, but s-maxage in the final Cache-Control response still proves the route is intentionally shared-cacheable.