Introduction
Caching can make a Next.js application faster and reduce unnecessary requests to databases and external APIs. It can also cause confusing bugs when users see stale content after a change.
The key is to understand what is being cached, how long it remains valid, and which event should invalidate it.
1. Identify the cache boundary
A modern web application can cache data at several levels:
- Browser or client-side caches
- CDN and HTTP caches
- Framework-managed data caches
- Rendered page or route output
- Database or application-level caches
These layers have different invalidation rules. Clearing one cache does not necessarily clear the others.
Next.js caching behavior has changed across releases, so consult the documentation for the version your project uses. Do not assume a caching default from an older tutorial applies to a newer application.
2. Make data-fetching intent explicit
A server component might retrieve a product from an API:
type Product = {
id: string;
name: string;
};
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const response = await fetch(
`https://api.example.com/products/${id}`,
{ cache: "no-store" }
);
if (!response.ok) {
throw new Error("Could not load product");
}
const product: Product = await response.json();
return <h1>{product.name}</h1>;
}
This example uses a request-time fetch configuration to avoid caching that request through the Next.js data cache. It assumes the endpoint returns the expected JSON shape and that the chosen Next.js version uses this params typing.
Use request-time fetching for data that must be fresh on each request, and use caching when repeated reads can safely reuse results.
3. Choose a revalidation strategy
Not all data needs the same freshness guarantee.
- Public documentation may tolerate relatively long cache lifetimes.
- A product description may be cached and revalidated after an edit.
- Inventory or financial data may need stricter freshness guarantees.
- Personalized data must not be shared across users through an incorrectly scoped cache.
Document the acceptable staleness for important data. This turns cache duration from a guess into an application requirement.
4. Invalidate the right data after mutations
After an administrator edits a product, the application may need to invalidate cached product data or rendered output. Next.js provides cache invalidation mechanisms such as revalidatePath and revalidateTag, with usage details depending on the framework version and caching model.
For example, a server-side mutation might invalidate a product page after a successful update:
"use server";
import { revalidatePath } from "next/cache";
export async function updateProduct(/* validated input */) {
// Authenticate and authorize the user.
// Validate input and persist the change.
revalidatePath("/products");
}
The function is intentionally schematic: the persistence logic, validation, and authorization must be implemented before use. Invalidating a path does not replace those security checks.
5. Test stale-data scenarios
Test what happens after updates, deletes, failed mutations, and concurrent edits. Confirm that private data cannot leak between users and that invalidation affects the intended pages or tags.
Measure cache hit rates and response times where possible. If an application is slow despite high cache usage, the bottleneck may be elsewhere.
Conclusion
Caching is a correctness decision as much as a performance decision. Define freshness requirements, understand the cache layers in your Next.js version, and invalidate data deliberately after mutations.