nextjs · caching · tutorial
Router Cache Confusing You? Next.js Caching, Fixed
fetch cache, RSC cache, router cache, revalidate — four caches, four rules. The table that ends the confusion, with BoringPush notes.

Four caches, one word "cache"
Struggling with router cache: a button calls an API, data changes, page shows the old row. Add Router needs better cache invalidation: revalidateTag runs, logs fetch, nothing updates. Route handlers caching despite no-store: cache: 'no-store' set, stale data anyway. Three threads, three different caches failing — all filed under "caching is broken."
The non-obvious truth, documented brutally in App Router RSC footguns in production: fetch({ cache: 'no-store' }) does NOT stop the RSC payload itself from being reused. You fixed one cache while staring at another.
The four caches and their one rule each
| Symptom | Guilty cache | Rule |
|---|---|---|
| API updated, page stale after navigation | Client router cache (30s staleTime) | router.refresh() after mutations, or revalidatePath from the action |
no-store fetch still returns old data in handlers |
Data cache semantics on route handlers | Don't fight it — move the read into the server component, or export const revalidate = 0 on the handler |
revalidateTag runs, page unchanged |
Full-route / RSC cache tag mismatch | Tag the fetch with next: { tags: ['posts'] } AND revalidate the same tag — mismatched names silently no-op |
| Page reloads fully after 30s idle | Router cache expiry (default 30s) | Expected. Lengthen with staleTimes in newer Next, or accept refetch |
force-dynamic page ignores revalidate |
You opted out of caching entirely | Remove force-dynamic, use granular revalidate = 300 per route instead |
Two patterns cover 90% of production pain. After any mutation, call revalidatePath('/posts') inside the server action — not in the client, not eventually.
And never scatter force-dynamic as a debugging reflex: it disables the caches whose knobs (revalidate, fetch options, unstable_cache for slow API hops) actually solve the problem, as the slow external API thread learned.
Where BoringPush helps is everything around the cache: local SQLite answers revalidations quickly, so cache misses cost little, and there is nothing cold to warm. Ship from anywhere on hardware where a miss is a rounding error, and caching becomes an optimization again instead of a load-bearing wall.
The 60-second triage ritual
Next stale report, run this order — it names the layer in under a minute:
- Same URL, hard reload. Fixed? Client router cache. Add
router.refresh()post-mutation. - Still stale? Check the fetch. Is the query tagged and is the tag revalidated with the identical string? Mismatched names are the #1 silent no-op.
- Still stale? Check the handler.
force-dynamicpresent? Replace with route-levelrevalidate = 300and re-test. - Slow API behind it all? Wrap just that hop so one sluggish CMS doesn't poison every page:
import { unstable_cache } from "next/cache";
const getPosts = unstable_cache(
async () => db.post.findMany({ orderBy: { createdAt: "desc" } }),
["posts-list"],
{ revalidate: 300, tags: ["posts"] }
);One cached hop, one tag, one revalidation path. That pattern ends more "caching is broken" threads than any other single change.
When to stop caching and fix the query instead
Caching has a smell test: if the same page needs three stacked cache layers to feel fast, the query underneath is the problem, not the cache config. The usual suspects from the threads — unindexed ORDER BY createdAt, N+1 Prisma includes on list pages, full-table CMS pulls filtered in JavaScript — all survive every cache setting and rot quietly until the cache expires at the worst moment.
Before adding another layer, run the slow query raw: over 200ms on local SQLite means fix the index first (the repo's database notes cover the exact PRAGMAs and the EXPLAIN QUERY PLAN check). Cache fast queries to skip work; never cache slow queries to hide them.
That one discipline removes half the entries in any caching confusion thread — and the half that remains is usually a mismatched tag name.
FAQ
revalidatePath vs revalidateTag — which one?
Path for page-shaped invalidation ("this URL is stale"), tags for data-shaped ("all pages using posts are stale"). Tag every fetch you plan to revalidate, or tags silently do nothing.
Why does no-store not prevent stale UI?
It controls the fetch data cache, not the rendered RSC payload or the 30s client router cache. Stale UI after correct fetching = one of the other two.
Is force-dynamic ever right?
Rarely: truly per-request pages (live dashboards). Everywhere else it trades one stale bug for permanent slowness.
Prefer route-level revalidate.
How do I debug which cache served stale?
Open DevTools, throttle to Slow 3G, navigate: instant = router cache hit; spinner then stale = RSC/data staleness; full reload = expired or uncached. Fix the layer you just identified.
Does BoringPush change any caching semantics?
No — same Next.js, same rules. It changes the cost of a miss (0.1ms local reads) so you can cache less aggressively and debug calmly.
+ all the boring stuff (analytics, auth, SQLite ↔ Supabase, full marketplace, webhooks)
Command from your phone.
Built so autonomous AI agents build what you want without breaking what's already paying you.
- Built-in analytics — cookie-free mode, $0 forever, no PostHog bill
- SQLite ↔ Supabase: one setting, same field names
- Full digital marketplace & feedback boards included
- 3 payment rails: Stripe, LemonSqueezy & Dodo Payments
- Agent fences: lint gate, read-only data access, auto-rollback
- Ship from your phone via SSH & WireGuard/Tailscale in under a minute
$199
Get BoringPushOne-time payment · Lifetime updates · Unlimited projects.
Keep reading


