# 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.

Source: https://boringpush.com/blog/struggling-with-router-cache-fix

## Four caches, one word "cache"
[Struggling with router cache](https://www.reddit.com/r/nextjs/comments/18dxlmw/struggling_with_router_cache): a button calls an API, data changes, page shows the old row. [Add Router needs better cache invalidation](https://www.reddit.com/r/nextjs/comments/13soigd/add_router_needs_better_cache_invalidation): `revalidateTag` runs, logs fetch, nothing updates. [Route handlers caching despite no-store](https://www.reddit.com/r/nextjs/comments/17izc0n/route_handlers_is_caching_fetched_data_even_if_i): `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](https://www.reddit.com/r/nextjs/comments/1pok7xu/non_obvious_app_router_rsc_footguns_we_hit_in): `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](https://www.reddit.com/r/nextjs/comments/1rpf7vs/caching_slow_external_api_route) 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:

1. **Same URL, hard reload.** Fixed? Client router cache. Add `router.refresh()` post-mutation.
2. **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.
3. **Still stale? Check the handler.** `force-dynamic` present? Replace with route-level `revalidate = 300` and re-test.
4. **Slow API behind it all?** Wrap just that hop so one sluggish CMS doesn't poison every page:

```js
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.
