From Full Pages to Fine-Grained Data: Rethinking ISR
For the past few years, the standard approach to scaling static content on the web has been Incremental Static Regeneration (ISR). It let developers pre-render pages at the speed of static while updating them without a full redeploy. The model worked well for sites with thousands or millions of pages, but it came with structural limits. Invalidations happened at the route level, the cache did not survive new deployments, and mixing personalized or real-time data with static content meant making compromises.
Next.js 13.2 introduces the Next.js Cache (beta), a rewrite of that older mechanism that moves caching down from the page level to the individual data-fetching layer. It is powered on the platform side by the Vercel Data Cache (beta), a shared backend cache built to work not only with Next.js but with any frontend or fullstack framework. The high-level change: you no longer need to choose between fully static and fully dynamic routes. A single page can now contain some permanently cached fetches alongside others that pull fresh, user-specific data on every request.
What ISR Accomplished—and Where It Fell Short
ISR, released by Next.js and Vercel in 2020, solved a pair of practical problems. It made page loads consistently fast by caching generated pages on Vercel's Edge Network and persisting them in durable storage. It also kept builds manageable as applications grew, allowing pages to be generated on request or through an API rather than strictly at build time.
The original developer experience relied on page-level exports to control revalidation:
export async function getStaticProps() {
let product = await getProduct()
return {
props: { product },
// Next.js will attempt to re-generate the page:
// - When a request comes in
// - At most once every 10 seconds
revalidate: 10, // In seconds
}
}
// Generates `/products/1` and `/products/2`
export async function getStaticPaths() {
// The developer has full flexibility to control
// what pages are generated during the build or on-demand
// For example, you could only generate the top products
// const topProducts = await getTopProducts()
return {
paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
fallback: 'blocking', // generate new product pages on-demand
}
}
Next.js 12.1 added programmatic invalidation through on-demand ISR, letting developers purge a specific statically generated route and have the update propagate globally within about 300 ms:
export async function getStaticProps() {
let product = await getProduct()
return {
props: { product },
// `revalidate` is no longer needed, content is only updated
// when programmatically invalidated through the API
// revalidate: 10
}
}
export default async function handler(req, res) {
// ...
await res.revalidate('/products/1');
// ...
}
These improvements were meaningful, but the architecture still left questions unanswered. Invalidating an entire route just to update a single component was heavy-handed. The cache could not be reused across deployments. There was no way to re-key cached content or to have part of a page render conditionally based on the visitor. The demand was clear: developers wanted dynamic rendering with the performance of static output.
A New Data-Fetching Foundation in the App Router
The reappraisal of ISR could not have happened without Next.js 13's App Router, which sits on a server-first programming model with colocated data fetching. Rather than relying on Next-specific functions like getStaticProps or getServerSideProps, developers can now write familiar async / await code to pull data directly inside pages, layouts, and Server Components, often simply by using the Web fetch API:
export default async function Page() {
let notes = await db.query('select * from notes');
return (
<ul>
{notes.map((note) => (
<li key={note.id}>{note.body}</li>
))}
</ul>
);
}
export default async function Page() {
const [staticData, dynamicData, revalidatedData] = await Promise.all([
// Cached until manually invalidated
fetch(`https://...`),
// Refetched on every request
fetch(`https://...`, { cache: 'no-store' }),
// Cached with a lifetime of 10 seconds
fetch(`https://...`, { next: { revalidate: 10 } }),
]);
return <div>...</div>;
}
The change is not merely stylistic. By default, fetch in the App Router will fetch and cache data automatically. Similar to extending the Fetch API the way a Service Worker does, the Next.js Cache extends the fetch options object so each request can customize its own caching and revalidating behavior. Sensitive data that Next.js can infer, such as requests carrying Authorization headers, remains uncached by default. Any request can still explicitly opt in to caching, but the defaults err on the side of safety. The docs recommend adding appropriate headers to sensitive requests to avoid accidental caching.
Next.js Cache: Granular Revalidation at the Fetch Level
Colocating data fetching with the components that consume it made the next generation of ISR possible. Static and dynamic fetches can now coexist within a single route, with cache control applying per request instead of per page. Individual fetches have their own lifecycle:
async function Tweet({ id }) {
// Tweets are static unless programmatically revalidated
let tweet = await fetch(`https://.../${id}`).then(res => res.json());
return (
<div>
<p>{tweet.author}</p>
</div>
);
}
async function Categories() {
// Fetch the latest categories at most every 60 seconds
let trendingCategories = await fetch('https://...', {
next: { revalidate: 60 },
}.then(res => res.json());
return (
<ul>
{trendingCategories.map((category) => (
<li key={category.id}>{category.name}</li>
))}
</ul>
);
}
That granularity changes what a redeployment means for cached content. In the older ISR model, code changes and data changes were entangled in the generated page cache. With the Next.js Cache, the two decouple. A redeployment of code no longer requires regenerating static pages from scratch because page generation can reuse the existing cache.
Behind the Scenes on Vercel and Self-Hosted
How the Next.js Cache behaves depends on where you run it.
On Vercel, the Vercel Data Cache handles the heavy lifting. For entirely static pages, the behavior resembles the ISR of today with several platform improvements. Cache lookups that miss at the regional level fall back to a single global bucket, effectively providing built-in "cache shielding" that raises hit ratios and speeds up visiting clients worldwide. Generated pages persist in durable storage, so instant rollbacks can happen without dropping previously cached entries. When content is invalidated, the Vercel Cache API broadcasts the update across all edge regions with propagation completing within roughly 300 ms.
The more interesting path is a mixed workload—some static fetches, some dynamic ones, all on one page. The Next.js Cache with the Vercel Data Cache keeps those granular entries in a fast, regional cache close to each visitor. A revalidation does not purge the entry immediately; it marks it stale. The subsequent visitor request is then served stale data while a background fetch goes to the origin to fetch the fresh value and repopulate the cache.
The system also works for self-hosted Next.js. In that setup an LRU cache is used, defaulting to 50 MB with the option to increase it. Entries are written to disk by default, and the filesystem cache can be shared across nodes when they agree on the cache key—the same basic approach as self-hosted ISR. Developers who want more control can modify the underlying cache keys, decide where entries are persisted, or disable persistence altogether.
Tag-Based Revalidation Is on the Roadmap
The core Next.js Cache is available in beta today for experimentation. Two additional APIs for programmatic updating are in the works. They target invalidation by path name or by cache tags:
revalidatePath(`/posts/[slug]`);
revalidateTag(`post-${id}`);
The tag-based approach adds an explicit label to the options object of an individual fetch request. This allows revalidating a specific piece of data across a potentially wide set of paths, rather than hitting an entire route or waiting out a time-based revalidation:
fetch(url, { next: { tags: [...] } });
What to Expect Right Now
The Next.js Cache is opt-in and only functions when static and dynamic workloads actually share the same route. Pure static routes on Vercel continue to behave exactly as page-level ISR does today. During its beta period, the maximum item size in the Vercel Data Cache is capped at 1 MB; expanded storage and larger cache sizes are slated to arrive with general availability.



