Field note / nextjs

publishedupdated 2026-05-04#nextjs#caching#revalidation#isr#app-router

Next.js Caching & Revalidation

The App Router cache model I actually use in production: route-level freshness strategy, fetch and tag invalidation, and read-your-own-writes after mutations.

TL;DR

  • In App Router, caching is an architecture decision, not a cleanup detail. Reads, writes, and invalidation should be designed together.
  • I decide freshness at the route and data-source level: static when possible, request-time when required, and explicit invalidation after writes.
  • Current Next.js docs separate broad route rendering choices from finer-grained cache APIs like revalidatePath, revalidateTag, updateTag, and cacheTag.
  • The hardest production bugs are rarely “too little caching.” They are “the wrong thing stayed cached after a mutation.”

Context

Caching in Next.js looks simple until the app grows:

  • marketing content wants static speed
  • product lists want tagged revalidation
  • account pages need user-specific freshness
  • mutations need read-your-own-writes
  • teams start mixing old and new cache mental models

I want every route to have an explainable freshness strategy before stale data bugs show up in production.

The Approach

1. Decide freshness first, API choice second

This is the model I use:

I start with the freshness requirement, then choose the specific cache and revalidation APIs that support it.

Typical route decisions:

Route typeMy defaultWhy
docs / knowledge pagestatic or long-lived cachecontent changes infrequently
catalog / listingcached with tag invalidationhigh read volume, acceptable stale window
user accountrequest-timepersonalized and correctness-sensitive
post-mutation confirmationimmediate cache updateread-your-own-writes matters

2. Be precise about what is cached

Current Next.js docs make an important point: in the App Router model, fetch requests are not cached by default, but routes can still be prerendered and their HTML cached.

That means I separate these questions:

  • Is the route itself static or dynamic?
  • Is this specific data fetch cached?
  • If cached, what invalidates it?

Examples:

await fetch("https://api.example.com/docs", {
  cache: "force-cache",
})
await fetch("https://api.example.com/account", {
  cache: "no-store",
})

3. Use tags when many surfaces depend on the same data

For shared data like products, docs, or notes, tags scale better than path-only thinking.

import { cacheTag } from "next/cache"

export async function getProducts() {
  "use cache"
  cacheTag("products")
  return db.product.findMany()
}

Then after a write:

import { revalidateTag, updateTag } from "next/cache"

revalidateTag("products", "max")
updateTag("product-123")

The nuance matters:

  • revalidateTag("products", "max") is good when stale-while-revalidate is acceptable
  • updateTag("product-123") is better for read-your-own-writes from a Server Action

4. Use path invalidation when the route boundary is the thing that changed

revalidatePath() is still useful when the page or layout itself is the cleanest invalidation unit.

import { revalidatePath } from "next/cache"

export async function publishArticle(id: string) {
  await db.article.update({
    where: { id },
    data: { status: "published" },
  })

  revalidatePath("/knowledge")
  revalidatePath(`/knowledge/article/${id}`)
}

This is often the clearest choice for route-driven content surfaces.

5. Keep the write path and invalidation plan in one conversation

I never want a mutation implemented without answering:

  • what becomes stale?
  • who needs to see the new value immediately?
  • is background revalidation okay?
  • is the invalidation keyed by route, data tag, or both?

That design discussion prevents a lot of ad hoc “just refresh the page” fixes later.

6. Acknowledge the evolving Next.js cache story

The current docs distinguish between the newer Cache Components model and the previous model. I care more about the durable design rules than the shifting API labels:

  • know what can be static
  • know what must be dynamic
  • tag shared data intentionally
  • make write invalidation explicit
  • optimize for correctness before cleverness

Trade-offs

  • More caching improves throughput and cost, but stale data bugs become easier to create and harder to diagnose.
  • More request-time rendering improves correctness, but I pay in latency and infrastructure spend.
  • Tag-based invalidation scales well, but teams need strict naming discipline or cache behavior becomes guesswork.
  • Route-level invalidation is simple, but it is often too coarse once many surfaces depend on the same underlying dataset.

References