
I spent an afternoon debugging a component that kept re-fetching on every single request. It had...
I spent an afternoon debugging a component that kept re-fetching on every single request.
It had 'use cache' right there in the code. I was confident it was working. It wasn't.
The problem was placement. 'use cache' was on the wrapper function, not inside the actual data function. That one mistake makes Next.js ignore the directive entirely. No error, no warning, nothing in the terminal. Just a function running on every request when it should have been cached.
Another time I wrote this in a Server Action during a Next.js 16 migration:
revalidateTag('products')
It compiled. It deployed. Pages stopped reflecting mutations. Calling revalidateTag without a second argument is a TypeScript error in Next.js 16, but the runtime fell back to legacy behaviour silently. I only caught it when users started reporting stale data.
Next.js 16's new caching model is genuinely great. But during development it is a complete black box. You add the directive, you assume it works, and you only find out otherwise when something breaks in production.
So I built a small dev-only toolkit to make it visible.
1. Silent cache misses
If a function runs more than once with identical arguments, you see this immediately:
[cache-debug] ⚠ POSSIBLE CACHE MISS - RE-EXECUTION WITH SAME ARGS
fn: getProductById
args: ["prod-123"]
This function ran 2 times with identical args.
If you expect caching: check 'use cache' is inside this function, not the wrapper.
That warning would have saved me that entire afternoon.
2. Dynamic holes from short cacheLife
cacheLife('seconds') silently excludes a component from the PPR static shell. It becomes fully dynamic. No warning anywhere, just a slower page that you cannot explain.
[cache-debug] ⚡ DYNAMIC HOLE WARNING
fn: getLivePrice
cacheLife 'seconds' is short-lived (< 5 minutes or revalidate: 0).
Next.js 16 automatically EXCLUDES this from the PPR static shell.
This function will run at request time, it is NOT prerendered.
Fix: Use 'minutes' or longer if you want it in the static shell.
3. Missing cacheTag
A cached function with no cacheTag() can only expire by time. You cannot revalidate it on demand. Easy to miss when moving fast, painful to discover later.
[cache-debug] 🏷 MISSING cacheTag WARNING
fn: getProductById
No cacheTag() found. This function cannot be invalidated on demand.
It will only expire when cacheLife runs out.
Fix: Add cacheTag('your-tag') inside the function.
4. Deprecated revalidateTag
In Next.js 16, revalidateTag('tag') without a second argument is a TypeScript error. The logInvalidation helper catches it before your CI does:
[cache-debug] ✗ DEPRECATED revalidateTag - MISSING SECOND ARG
tag: products
revalidateTag('products') without a profile is deprecated in Next.js 16.
Fix: revalidateTag('products', 'max')
5. updateTag outside a Server Action
Calling updateTag outside a Server Action throws at runtime. The toolkit catches it at dev time before it reaches production.
6. Repeated fetches
detectRepeatedFetch surfaces the same URL being hit multiple times in one render. Usually means a cache layer is missing entirely.
Step 1: Enable in .env.local
CACHE_DEBUG=true
Do not add this to .env.production. The NODE_ENV guard already ensures it is off in production, but keeping env files clean is good practice.
Step 2: Wrap your cached functions
The 'use cache' directive must stay inside the original function. withCacheDebug is a regular wrapper and cannot be a cache boundary. If you put 'use cache' on the wrapper, the instrumentation gets cached instead of the data function, which is exactly the mistake the POSSIBLE CACHE MISS warning is designed to catch.
import { cacheLife, cacheTag } from "next/cache";
import { withCacheDebug } from "@/lib/cache-debug";
async function _getProductById(id: string) {
"use cache";
cacheLife("hours");
cacheTag(`product-${id}`, "products");
return db.query("SELECT * FROM products WHERE id = $1", [id]);
}
export const getProductById = withCacheDebug(_getProductById, {
name: "getProductById",
cacheLife: "hours",
tags: ["product-{id}", "products"],
});
const product = await getProductById("prod-123");
Zero API change. The exported function works exactly the same everywhere you already call it.
Step 3: Log invalidation calls in Server Actions
"use server";
import { revalidateTag, updateTag } from "next/cache";
import { logInvalidation } from "@/lib/cache-debug";
export async function updateProductPrice(id: string, newPrice: number) {
await db.query("UPDATE products SET price = $1 WHERE id = $2", [newPrice, id]);
logInvalidation("updateTag", `product-${id}`, {
isServerAction: true,
context: "admin price update",
});
updateTag(`product-${id}`);
logInvalidation("revalidateTag", "products", {
profile: "max",
isServerAction: true,
context: "admin price update",
});
revalidateTag("products", "max");
}
None of these affect production because the tool is not present there.
| What it detects | Without this tool |
|---|---|
| FIRST RUN / CACHE MISS / NEW KEY | Not visible |
| Dynamic hole from short cacheLife | Not visible |
| Missing cacheTag | Not visible |
| Deprecated revalidateTag | TypeScript error, easy to miss |
| updateTag outside Server Action | Runtime throw |
| Repeated fetches in one render | Not visible |
Zero external dependencies. TypeScript 5.0+ with strict mode. Next.js 16 only -- updateTag does not exist in Next.js 15. Double-gated on NODE_ENV === 'development' AND CACHE_DEBUG=true so nothing ships to production. No overhead, no bundle impact.
Free forever, one .tsx file: shubhra.dev/snippets/nextjs-use-cache-debugger
If you want the production enforcement layer that pairs with this -- type-safe tag registry, safeRevalidate that blocks the deprecated single-arg call at compile time, serverActionInvalidate that enforces the correct invalidation order -- that is the Cache Pro Kit.
If you are new to the Next.js 16 caching model and want to understand what 'use cache', cacheLife, and cacheTag are actually doing before using this toolkit, the practical migration guide covers the full picture. There is also a 15-question quiz if you want to test your understanding.
If you are in the middle of a Next.js 16 caching migration and something is behaving unexpectedly, this will make it visible. What has been the most frustrating or confusing part of the new caching model for you?
aiMost of us have seen a coding agent fail to complete a task we know it can do. We just don't...
googlecloudWhen building Generative AI applications, developers often encounter a massive bottleneck: sequential...
discussI’ve been thinking about sharing some electronic circuit posts on Dev.to — small circuits, DIY...
agentsWhat nobody tells you about exporting your multi-agent prototype to a local workspace. Every...
agenticarchitectAutonomous agents are genuinely good at answering messy business questions. Give one an LLM and a set...
aiPR volume went up, ticket quality didn't, and the gap got filled with LLMs on both sides of the review: bots reviewing, bots replying, bots occasionally arguing with bots about priorities that only existed in a teammate's head. Our CEO named the actual problem, and it's bigger than code review.
Workflows from the Neura Market marketplace related to this Stable Diffusion resource