Verified against Claude Code · 2026-07-25
Catch the Date.now()-in-cache trap before Cache Components ships it to production
Audits routes and 'use cache' functions for the frozen-timestamp trap and cross-user cache-key leaks under the Cache Components model, where dynamic is now the default and caching is the explicit opt-in.
The prompt
Ready to copy — highlighted parts are example details you can swap.
You are auditing Next.js code for caching correctness under the Cache Components model, where a component or function is dynamic — it re-runs on every request — by default, unless it explicitly opts into caching with a 'use cache' directive at the top of the file or the function. Treat that inversion as the baseline for this audit; do not assume the older static-unless-opted-out model still applies anywhere in this codebase. CACHING MODE AND VERSION Next.js 16.1, cacheComponents: true in next.config.ts CODE TO AUDIT app/account/page.tsx plus lib/get-account-summary.ts, which is wrapped in 'use cache' and calls new Date() to build a 'balance as of' string. FRESHNESS REQUIREMENTS Account balance must always be current; the 'member since' date can be cached indefinitely; notification counts can be up to 2 minutes stale. KNOWN CACHE-RELATED INCIDENTS A support ticket reported two different users seeing the same account balance figure for about six minutes after a manual cache warm-up script ran. THE TRAP TO CHECK FOR Inside every function or component marked 'use cache', search for a call to a dynamic API: Date.now(), new Date(), Math.random(), cookies(), headers(), or a searchParams value read directly inside the cached scope. Flag every instance as a bug, not a style note. Once a function is cached, whatever value one of these calls happened to return on the request that populated that specific cache entry is frozen into every response served from that entry until its cacheLife profile expires or a matching cacheTag triggers revalidation — a "last updated" timestamp, a per-user greeting, or a randomized A/B assignment computed inside a 'use cache' scope will show the same frozen value to every visitor who hits that cache entry, not just the first one who happened to populate it. For every instance found, state exactly what value gets frozen, for how long given any cacheLife profile that's actually set (or the platform default if none is), and the fix: either move the dynamic read outside the 'use cache' boundary and pass it in as an argument from the uncached caller, or confirm explicitly that the staleness is genuinely intended and the cacheLife profile is scoped tightly enough to match that intent. OTHER CHECKS Confirm every 'use cache' function has an explicit cacheLife() profile rather than silently relying on the platform default — call this out either way, since an unnamed default is itself a decision nobody actually made on purpose. Check that cacheTag() calls are specific enough to invalidate correctly given the freshness requirements above — a single global tag covering an entire collection means any one item's update invalidates everything cached under that tag, which technically works but defeats the purpose of caching at the granularity the underlying data actually changes at. Flag anything cached that reads request-specific data — a user ID, a session token, a role, a tenant identifier — without that value being part of what determines the cache key, since that is a cross-user cache leak: one user's cached response getting served back to a completely different user who happens to hit the same cache entry next. OUTPUT FORMAT A findings table: File or function | Issue | Severity — bug, needs-profile, or leak-risk | Fix. Follow the table with one paragraph stating plainly whether the current caching setup, taken as a whole, actually matches the stated freshness requirements, and naming the single highest-risk finding first if more than one bug was found.
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
This targets one specific, well-documented failure mode instead of asking generically to "review the caching," a prompt vague enough that a model can nod along without checking anything concrete. The Cache Components model inverts the old default — static unless opted out via export const dynamic — into dynamic unless opted in via 'use cache', and that inversion is exactly where the Date.now() trap lives: code written under the old mental model assumed a timestamp read at request time would just work, and it did, right up until that same function got wrapped in 'use cache' for a performance win and the timestamp froze into the cache entry instead of staying live. Naming the exact dynamic APIs to search for — Date.now, new Date, Math.random, cookies, headers, a direct searchParams read — gives the model a grep-able checklist instead of a vibe, which matters because these calls look completely ordinary in isolation; nothing about new Date() looks dangerous unless the reviewer specifically knows it is sitting inside a cached scope, which is precisely the kind of local-looks-fine, global-is-broken bug that a line-by-line read easily misses without being told to look for this exact pattern. The cross-user cache-leak check catches a second, distinct failure mode that the timestamp trap doesn't cover at all: caching is correct in the sense that nothing froze incorrectly, but the cache key itself doesn't include the identity that actually varies the response, so one user's fetched data gets served to the next user who happens to land on the same cache entry — a bug that a staleness-only audit would walk right past because nothing in it is stale, it's simply attributed to the wrong person. Requiring an explicit statement of whether every 'use cache' function has a named cacheLife profile, rather than silently accepting the default, forces a decision that's easy to skip during a first pass at adopting the model — an unset profile isn't neutral, it's a real default duration that nobody chose on purpose, and surfacing that as its own checkable item is what stops it from being discovered later as a production incident instead of a review comment.
What you get back
lib/get-account-summary.ts, marked 'use cache': calls new Date() to compute "balance as of {date}" — frozen at whatever moment first populated the cache entry. No cacheLife() is set, so it falls back to the platform default profile, meaning every visitor who hits that cache entry sees a stale "as of" timestamp presented as current until the entry expires. Fix: pass the current timestamp in as an argument from the calling Server Component, which is uncached, or drop caching on this specific field entirely if it must always be live. Summary: freshness requirements are not currently met — the balance figure itself is fetched fresh, but the "as of" label attached to it is cached and can silently misrepresent how current the balance actually is. Highest-risk finding: the frozen timestamp, since it directly contradicts the stated "must always be current" requirement for balance-related data.
Verified against
Claude Code Sonnet 4.6 · 2026-07-25
Claude Sonnet 4.6 · 2026-08-01
Changelog
- 2026-07-25 — Initial publish, verified against Claude Code and Claude (Sonnet 4.6) on Next.js 16.1 Cache Components.
- 2026-08-01 — Added the cross-user cache-key leak check after a review missed a session-scoped value cached without the session in the key.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
