Next.js Caching Explained (and How to Control It)

Next.js caching operates across several scopes: request memoization during a server render, persistent data or function caching, cached route output or prerendered shells, and the client-side router cache. You control each scope differently, using fetch cache options, revalidation settings, Cache Components APIs, route invalidation, or router.refresh().
The exact defaults depend on your Next.js 16 configuration. Projects using Cache Components opt specific routes, components, or functions into caching with "use cache". Projects using the previous model can cache fetch results with cache: "force-cache" or next.revalidate.
Next.js caching means storing data, computed output, rendered route content, or client navigation payloads so repeated work can be skipped until the cached entry expires or is invalidated.
Next.js caching layers at a glance
The phrase “Next.js cache” is misleading because it refers to several separate mechanisms. Clearing one layer does not automatically clear every other layer.
Layer | What it caches | Scope | How to control it |
|---|---|---|---|
Request memoization | Duplicate | One render pass | Reuse identical request arguments. The memoized entry disappears after rendering |
Data or function cache | Server-side fetch results or cached function output | Across requests and deployments, depending on the runtime and cache handler | Previous model: |
Full route cache or prerendered shell | HTML and React Server Component output generated ahead of a request | Server-side route output | Choose static or dynamic rendering, revalidate the underlying data, or invalidate the route |
Router cache | Prefetched and previously visited React Server Component payloads | Current browser session |
|
Application cache | Sessions, rate limits, expensive query results, shared state, or computed values | Across application instances and workers | Redis keys, TTLs, explicit deletion, versioned keys, and application-owned invalidation |
Note: Next.js 16 supports two related models. Cache Components is the newer opt-in model. The previous model still uses
fetchcaching, route segment configuration, and APIs such asunstable_cache, although Next.js recommends"use cache"for new Cache Components code.
Request memoization only lasts for one render
Next.js can deduplicate identical GET requests made with fetch while React renders a server component tree. If generateMetadata(), a layout, and a page request the same resource with identical arguments, the server can perform the request once during that render.
That behavior is request memoization, not a persistent data cache. The entry is discarded when the render finishes, so a later visitor can trigger the request again.
Request memoization also does not automatically apply to arbitrary database calls. If three components each run the same Prisma query, do not assume Next.js will merge those calls. Put the query behind a shared data function, then choose whether the function should remain dynamic or be cached.
Control data caching and revalidation
Projects using the previous caching model
In the previous model, server-side fetch requests are not cached by default. Use cache: "force-cache" for persistent caching, cache: "no-store" for fresh data, or next.revalidate for a time-based policy.
In app/products/page.tsx, a five-minute revalidation window can be set on the request:
type Product = {
id: string
name: string
}
async function getProducts(): Promise<Product[]> {
const response = await fetch("https://api.example.com/products", {
next: {
revalidate: 300,
tags: ["products"],
},
})
if (!response.ok) {
throw new Error(`Product request failed with ${response.status}`)
}
return response.json() as Promise<Product[]>
}
export default async function ProductsPage() {
const products = await getProducts()
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}
A revalidation value of 300 means the cached entry may be reused for five minutes before Next.js fetches updated data. This is appropriate for a product catalogue that tolerates a short delay. It is a poor policy for a payment status, permission check, or inventory value that must reflect the latest write.
For data that must be requested on every render, set cache: "no-store":
const response = await fetch("https://api.example.com/account", {
cache: "no-store",
})
Projects using Cache Components
With cacheComponents: true, data fetching is dynamic unless you mark a route, component, or function as cacheable. The "use cache" directive defines the cached boundary, while cacheLife() sets its lifetime and cacheTag() links it to an invalidation tag.
In lib/products.ts:
import { cacheLife, cacheTag } from "next/cache"
export async function getProducts() {
"use cache"
cacheLife("hours")
cacheTag("products")
return db.product.findMany({
orderBy: { createdAt: "desc" },
})
}
The main advantage is placement. You can cache a database function or component without pretending that the whole route has one freshness requirement. A pricing panel can remain dynamic while a documentation sidebar is cached.
Do not enable Cache Components and then copy older caching advice line by line. The APIs overlap in purpose, but the mental model changes from implicit route behavior toward explicit cached boundaries.
Use time-based and event-based revalidation for different problems
Time-based revalidation is appropriate when staleness has a known tolerance. Blog lists, documentation indexes, public profiles, and catalogue pages commonly fit this model.
Event-based revalidation is a better fit when a mutation tells you exactly when cached data became stale. After an editor publishes a post, invalidate the affected tag or path rather than waiting for the next timer.
In app/actions/products.ts, a Server Action can mark tagged product data as stale:
"use server"
import { revalidateTag } from "next/cache"
export async function updateProduct(
productId: string,
input: { name: string },
) {
await db.product.update({
where: { id: productId },
data: input,
})
revalidateTag("products", "max")
}
Using the "max" profile applies stale-while-revalidate behavior. The next visit can receive stale data while Next.js refreshes the tagged entry in the background.
Use revalidatePath("/products") when the invalidation target is a specific page or layout. Use tags when the same data appears across several routes, such as a product card shown on the home page, category pages, search results, and the product page.
A practical rule is to invalidate close to the write. The code that changes a product already knows which product, list, workspace, or tenant became stale. Moving invalidation into unrelated page code increases the chance that a mutation path gets missed.
Full route caching depends on rendering mode
Under the previous model, statically rendered routes can be stored as HTML and React Server Component payloads in the Full Route Cache. Dynamically rendered routes are generated at request time instead.
The Data Cache and Full Route Cache are connected but not identical. Revalidating cached data can cause a route to render again and replace its cached output. Invalidating only the browser router does not erase persistent server data.
Cache Components changes the presentation of this model. Next.js can prerender a static shell, include cached components inside it, and stream dynamic content when the request arrives. That lets one route contain content with different freshness requirements.
Do not force a dashboard into static rendering merely to improve a synthetic benchmark. A dashboard that depends on the current user, workspace permissions, live billing state, and recent activity usually needs dynamic boundaries. Cache only the expensive shared work that has a safe reuse policy.
Opt out when correctness matters more than reuse
Caching is a policy decision, not a default performance checkbox. Before caching a value, define these four properties:
Cache key: Which inputs produce a distinct value?
Freshness window: How stale may the value become?
Invalidation event: Which write makes the value incorrect?
Failure behavior: What happens when the cache is unavailable?
Personalized and authorization-sensitive data deserves stricter handling. A cache key for workspace data must include the workspace identifier and every input that changes the answer. Omitting tenant context can expose one customer’s data to another customer.
Skip persistent caching for permission checks, payment confirmation screens, single-use tokens, and mutation responses unless the design explicitly preserves correctness. A fast stale answer is still a wrong answer.
router.refresh() is useful after a client-side action when the current route should request a new server payload. It does not replace server-side invalidation. If the underlying data or function cache still contains the old value, a refreshed route can render the same stale data again. The Server Action refresh() API similarly refreshes the client router from a Server Action.
Use Redis for application-owned caching
Next.js framework caching is useful for route rendering and server data access inside the Next.js application. Redis solves a different problem: a cache or shared state store owned by your application and available to multiple processes.
Redis is a practical choice when an Express API, Next.js frontend, and background workers need the same cached values. Common examples include session lookups, rate-limit counters, expensive aggregate queries, short-lived API responses, and distributed locks. Redis documents caching as storing frequently accessed data in memory to reduce database work and improve response times.
A Redis cache needs an explicit key design. For a workspace dashboard, a versioned key could be:
workspace:{workspaceId}:dashboard:v3
The version suffix gives you a controlled escape hatch after changing the cached data shape. Use a TTL as a safety net, then delete or replace the key after writes that change the dashboard.
Redis should degrade safely. If Redis is unavailable, a read-through cache can fall back to PostgreSQL and return the uncached result. The fallback may be slower, but it should remain correct. Prevent a cache miss from becoming a full application outage unless the cached value is also the application’s required source of truth.
FastStaq uses Redis for application caching and BullMQ background queues. That separation matters because the Next.js router cache cannot coordinate jobs, share queue state with an Express API, or provide application-defined keys across workers.
For a direct comparison of external cache options, read Redis vs Memcached. For search rendering and indexability concerns, continue with Next.js SEO
FAQ
Does Next.js cache fetch requests by default?
In the current previous-model App Router documentation, server-side fetch requests are not cached by default. Add cache: "force-cache" or next.revalidate when you want reuse. With Cache Components enabled, data fetching remains dynamic unless code is placed inside a "use cache" boundary.
What is the difference between revalidatePath and revalidateTag?
revalidatePath targets a page or layout path. revalidateTag targets every cached entry associated with a tag, which is useful when the same data appears on several routes. Choose the invalidation unit that matches the write.
Does router.refresh() clear every Next.js cache?
No. router.refresh() requests a fresh server payload for the current route and updates the client view. It does not automatically remove persistent data cached on the server, so use revalidatePath, revalidateTag, or the relevant Cache Components API when server data changed.
Should I use Redis instead of the Next.js cache?
Usually, they serve different scopes. Use Next.js caching for framework-managed rendering and data reuse. Use Redis when several application processes need shared keys, TTLs, counters, sessions, cached queries, or queue infrastructure.
Can I cache authenticated pages?
Yes, but the cache boundary and key must preserve user and tenant isolation. Shared caching is unsafe when the cached output depends on cookies, headers, permissions, or private account data unless those inputs are handled by an appropriate private-cache design. Dynamic rendering is often the safer default for authenticated dashboards.
Where to go next
Treat caching as a set of boundaries rather than one global switch. Identify the work you want to reuse, define how stale it may become, and connect invalidation to the mutation that changes it.
For a new Next.js 16 project, evaluate Cache Components before adopting older route-level patterns. For an existing project, document which model is active before changing cache behavior.
Continue with the FastStaq SaaS boilerplate guide. FastStaq provides a Next.js 16 frontend, an Express TypeScript API, PostgreSQL through Prisma, Redis, and BullMQ workers as a production-ready SaaS foundation.


