How do you debug weird caching when users swear they refreshed?

Yo Kirupa folks, I’m migrating a frontend from REST to a CDN-fronted GraphQL API and I keep getting “it’s fixed for me but not for them” reports even after people hard refresh, and the failure mode feels like some combo of browser cache, service worker, CDN, and stale ETags fighting each other.

When you’re on-call and need signal fast, what’s your go-to observability/debugging strategy to pinpoint which layer served the stale response (browser vs SW vs CDN vs origin) without shipping a bunch of risky logging to prod?

Fastest signal for me is stamping every response with a provenance header that each layer sets or overwrites, then checking it in DevTools Network. Something like x-debug-cache: sw, then cdn-hit or cdn-miss, then origin. The last writer tells you who actually served it.

The part that stands out here is the hard refresh still not helping. That is usually the service worker still controlling the page and serving from Cache Storage. I would have users screenshot DevTools → Application → Service Workers plus Network showing from ServiceWorker versus a normal fetch. That one string saves me hours.

I have had similar problems, and the challenge is determining which cache is supplying the response to change before you do.

Here are a few things that have helped me to quickly distill it:

Open DevTools and disable the cache (check the Network tab). This eliminates the normal HTTP cache.
Temporarily bypass or unregister the service worker, and test again. If it doesn’t show, then you have ruled out the CDN and the origin as the main cause of the problem.
Check Cache-Control, ETag, Last-Modified, Age, CF-Cache-Status, X-Cache, etc. headers for any CDN-specific cache headers. Generally indicate origin or edge (from which they have emerged).
Make the same request with `curl` (no browser state) and compare with what the browser receives. When `curl` fetches something different from what the browser fetches, it is likely to be a client-side cache or service worker problem.
In addition, if possible, add a lightweight response header like build ID, deployment version, origin instance ID, etc. It introduces little risk and can make it much easier to determine if everybody is using the same version of the backend.
Do not add new application logging, correlate with request IDs in your CDN or origin logs. This is often sufficient visibility without a production code change.

In my experience, “stale content” issues typically become one layer that nobody thinks is a factor. It is much more efficient to delete each cache layer in turn, than to make random cache purges or redeployments.

If you can only do one “on-call sanity move”, add a cheap X-Build-Id (or X-Deploy-Sha) header on every GraphQL response and then have folks screenshot the response headers from DevTools. That instantly tells you if they’re actually hitting the new backend, or if you’re chasing ghosts in the browser/SW/CDN.

Kirupa has a solid walkthrough on using DevTools Network to see what’s cached and why (headers, disable cache, etc.): https://www.kirupa.com/html5/viewing_http_headers.htm

1 Like