Self-test: is caching working?
Press the button. This requests every rendering and caching route from the server twice, compares the execution stamp of every panel on it, and reports whether each stamp moving or holding was the correct answer for that route.
It is the same set of assertions scripts/cachecheck makes from outside the app, run from inside it against its own origin. Open a card to see what the check expected, the evidence behind the verdict, and the likely causes of a failure.
What to check
A red result is not always your bug. The first four checks rule out the usual false alarms: whether this is a dev server, whether the cache handler stores anything, and whether a CDN in front of the app is answering instead of the app.
Run it in pnpm build && pnpm start for a real verdict. Under next dev nothing is prerendered and nothing is prefetched, so the App Shell checks are skipped and build-dependent failures are reported as INFO rather than FAIL.
quick runs about 14 checks and full about 30, plus 5 invalidation scenarios if enabled. Most make two or more requests 250ms apart. The first run after a deploy or restart is the slowest, because every cache entry has to be filled from its upstream first.
How a verdict is reached
- PASS
- every panel did what the route requires. Stamps held where a cache hit was guaranteed and moved where nothing was cached.
- FAIL
- a panel broke a guarantee. The main one is that Cache Components guarantees a hit inside a profile’s
revalidatewindow, so a scope that re-runs there is a real fault rather than timing luck. - INFO
- the answer was legal either way. Usually a panel was already past its revalidate window, where regenerating is ordinary background work. Re-run to get a reading inside the window.
- SKIP
- the check needs a production build to mean anything.
Two things can give a wrong reading, both described in TESTING.md. A run is itself a visit, so the run triggers the App Shell upgrade. To see the first-visit path, check a cold URL with curl. A revalidation window can also expire mid-run and look like a blast radius, so each panel is judged by its own age against its own window.
From a shell
The same run streams as newline-delimited JSON, so it works in CI or a terminal:
curl -N localhost:3000/api/selftest?suite=quick
curl -N 'localhost:3000/api/selftest?suite=full' | jq -c 'select(.type=="result") | {v:.result.verdict, t:.result.title}'
# the invalidation scenarios mutate shared cache state, so they need a POST
curl -N -X POST 'localhost:3000/api/selftest?suite=quick&confirm=mutate-cache'GET never invalidates anything, and the POST is refused cross-site. The suite only requests a fixed list of paths, and only on loopback or a hostname listed in SELFTEST_ALLOWED_HOSTS. An unrecognised Host header falls back to this process’s own port instead of being followed. Set SELFTEST_DISABLED=1 to turn the endpoint off entirely, and SELFTEST_SHOW_ENV=1 to un-redact configuration values, which are reported only as set or not set outside dev.