Two types of sessions: stateless and database
Stateless sessions store session data or a token in the browser's cookies, which is sent with each request. Database sessions store session data in a database, with the user's browser only receiving the encrypted session ID. Stateless is simpler but can be less secure if not implemented correctly. Database sessions are more secure but can be complex and use more server resources. Session management libraries like iron-session or Jose are recommended.
Update or refresh session expiration
Extend the session's expiration time to keep the user logged in when they return to the application. Decrypt the current session from the cookie, verify it exists and is valid, then set the same session cookie with a new expiration date.
Session management libraries
Recommended session management libraries are Iron Session and Jose for encrypting/decrypting session data and managing cookies.
dynamic, revalidate, and fetchCache configs are replaced by use cache and cacheLife
When Cache Components is enabled, the route segment configs dynamic, revalidate, and fetchCache are replaced by the 'use cache' directive and the cacheLife() function. Routes that still export these configs will error after enabling cacheComponents.
dynamic = 'force-dynamic' is not needed with Cache Components
With Cache Components enabled, all pages are dynamic by default, so the export const dynamic = 'force-dynamic' config is no longer needed and should be removed.
Replace dynamic = 'force-static' with 'use cache' and cacheLife
When migrating from dynamic = 'force-static', remove the config and add 'use cache' with cacheLife() in the component. For uncached data access, add 'use cache' as close to the data access as possible with a long cacheLife like 'max'. For runtime data access (cookies(), headers(), etc.), wrap it with <Suspense>.
Replace revalidate config with cacheLife function
The revalidate export const should be replaced with the cacheLife() function. For example, export const revalidate = 3600 becomes 'use cache' with cacheLife('hours'). If your revalidate value doesn't match a built-in cacheLife profile ('seconds', 'minutes', 'hours', 'days', 'weeks', 'max'), pick the closest one or define a custom cache profile.
fetchCache config is not needed with Cache Components
The fetchCache config is no longer needed with Cache Components. Instead, use 'use cache' to control caching behavior. All data fetching within a cached scope is automatically cached, making fetchCache unnecessary.
Move fetch cache options to 'use cache' directive
With Cache Components, wrap fetches in a 'use cache' function instead of using cache: 'force-cache' option. The fetch next.revalidate and next.tags options become cacheLife() and cacheTag() functions inside the 'use cache' scope. Example: async function getData() { 'use cache'; cacheLife('hours'); cacheTag('data'); const res = await fetch('https://api.example.com/data'); return res.json(); }
'use cache' uses in-memory storage by default, unlike fetch Data Cache
'use cache' defaults to in-memory storage, so its entries are discarded when the serverless instance is destroyed and are scoped to a single deployment. The fetch Data Cache persists cached responses across deployments and across serverless instances. Use 'use cache: remote' or a cache handler for durable storage with 'use cache'.
Replace unstable_cache with 'use cache' directive
The unstable_cache API is replaced by the 'use cache' directive. Convert the wrapped function into a function with the 'use cache' directive. The cache key is derived automatically from the arguments (no key-parts array needed), and the options object maps to cacheLife() and cacheTag() functions.
updateTag vs revalidateTag for on-demand revalidation
For on-demand revalidation with Cache Components: use updateTag() for mutations whose result the user must see immediately (read-your-own-writes). Call it from a Server Action to expire the tag so the next request waits for fresh data. Use revalidateTag() for stale-while-revalidate, which requires a cache profile as the second argument (e.g., revalidateTag('posts', 'max')). Works in Server Actions and Route Handlers. revalidatePath() is unchanged from the previous caching model.
updateTag can only be called from Server Actions
The updateTag() function can only be called from a Server Action. Calling it elsewhere throws an error. In Route Handlers or webhooks, use revalidateTag() instead with a cache profile.
unstable_noStore is not needed with Cache Components
The unstable_noStore() (noStore) function is not needed with Cache Components. Nothing is cached unless you add 'use cache', so you can remove noStore(). If a component must run at request time, call connection() before the work and wrap it in <Suspense>.
Replace dynamic = 'force-static' in Route Handlers with 'use cache'
For GET Route Handlers, remove the export const dynamic = 'force-static' config and move the data access into a separate function marked with 'use cache' and cacheLife(). The directive can't be applied to the GET export itself, so the handler calls a cached helper function. Example: async function GET() { const products = await getProducts(); return Response.json(products); } async function getProducts() { 'use cache'; cacheLife('hours'); return db.query('SELECT * FROM products'); }
fetch requests no longer cached by default in Next.js 15
In Next.js 15, fetch requests are no longer cached by default. To cache specific requests, pass cache: 'force-cache' option. To cache all fetch requests in a layout or page, use export const fetchCache = 'default-cache' segment config. Individual fetch cache options override the segment config.
fetch caching example with force-cache
export default async function RootLayout() { const a = await fetch('https://...') // Not Cached; const b = await fetch('https://...', { cache: 'force-cache' }) // Cached }
fetchCache segment config for layout caching
export const fetchCache = 'default-cache' applies caching to all fetch requests in the layout and app that don't set their own cache option. Individual fetch requests with cache: 'no-store' will override this and not be cached.
Route Handler GET no longer cached by default in Next.js 15
GET functions in Route Handlers are no longer cached by default. To opt GET methods into caching, use a route config option such as export const dynamic = 'force-static' in the Route Handler file.
Client Cache page segments not reused on navigation in Next.js 15
Page segments are no longer reused from the Client Cache when navigating between pages via <Link> or useRouter. They are still reused during browser backward/forward navigation and for shared layouts. Use the staleTimes config option to opt page segments into caching.
staleTimes configuration for Client Cache in Next.js 15
export const staleTimes = { dynamic: 30, static: 180 } in next.config.js under experimental enables caching of page segments with specified stale times in seconds.
Layouts and loading states still cached in Client Cache
Unlike page segments in Next.js 15, layouts and loading states continue to be cached and reused during navigation.
Mark cacheable functions with 'use cache' directive
In server functions or components that fetch data, mark with 'use cache' to enable caching. Combine with cacheLife() to set cache duration and cacheTag() to assign cache invalidation tags. Example: 'use cache' followed by cacheLife('hours') and cacheTag('tasks', `task-${id}`).
Enable cacheComponents in next.config.ts
To use Cache Components feature (Next.js 16), set cacheComponents: true in next.config.ts. Enabling Cache Components applies prerender validation across every route. Routes that read request data such as cookies(), headers(), or searchParams outside <Suspense> will block prerendering and need to be split with Suspense boundaries.
Use updateTag to invalidate specific cached reads
In Server Functions that mutate data, call updateTag() with the cache tags that changed. Every mutation should identify its tags and invalidate them. Example: after updating a task status, call updateTag('tasks') and updateTag(`task-${taskId}`). This is more precise than refresh() which reruns all dynamic work.
Example: updateTag in mutation
```ts
'use server'
import { updateTag } from 'next/cache'
export async function updateStatus(taskId: string, newStatus: Status) {
// …mutate
updateTag('tasks')
updateTag(`task-${taskId}`)
}
```
Every Server Function mutation calls updateTag for cached reads that changed. Use updateTag for cached reads, refresh() for dynamic reads.
Example: Cached getTask function
```ts
import { cacheLife, cacheTag } from 'next/cache'
import { getTaskById } from '@/lib/db'
export async function getTask(id: string) {
'use cache'
cacheLife('hours')
cacheTag('tasks', `task-${id}`)
return getTaskById(id)
}
```
'use cache' marks function as cacheable. Per-id tag `task-${id}` gives single task its own handle. Broader 'tasks' tag covers list operations.
Cached reads enable instant prefetching
When reads are cached with 'use cache' and tags, prefetched output becomes reusable in Client Cache and tag-invalidatable. Dynamic reads can still resolve during prefetch but each prefetch does real server work. Combined with per-link prefetching, cached reads enable pages to paint instantly when user clicks.
Use refresh() for dynamic reads that cannot be tagged
For reads that depend on request data like cookies or headers and cannot be cached with tags, use refresh() in Server Functions to rerun those dynamic reads. updateTag() only works with cached reads using 'use cache'. Dynamic reads must use refresh().
Partial Prefetching invalidations
With Partial Prefetching enabled, data invalidations (revalidateTag, revalidatePath) silently refresh associated prefetches.
Client cache reuse for sibling routes
Next.js stores prefetched React Server Component payloads in memory, keyed by route segments. When navigating between sibling routes (e.g., /dashboard/settings → /dashboard/analytics), Next.js reuses the parent layout and only fetches the updated leaf page.
Cache session-dependent lookups by passing session value as parameter
To cache content that depends on cookies() or headers(), read the session value outside the cached function and pass it in as a parameter. The cache will key off the session value. Example: async function getTopics(team: string | undefined) { 'use cache'; return db.topics.forTeam(team); } Then call it with the resolved session value: const team = (await cookies()).get('team')?.value; const topics = await getTopics(team);
Partial Prefetching enables App Shell prefetching for Cache Components routes
When Partial Prefetching is enabled, a <Link> prefetches only the App Shell for its destination route, which contains static content and cached content that doesn't depend on the URL. Next.js builds one App Shell per route and reuses it for every link to that route, rather than prefetching each link separately as it did before. This feature only works when cacheComponents is enabled.
Per-link prefetching with prefetch={true} resolves URL-specific content
To prefetch more than the App Shell, a link can opt into per-link prefetching with <Link prefetch={true}>. This prefetch resolves URL-specific content that depends on params, searchParams, or the full URL, in addition to the App Shell.
Enable partialPrefetching in next.config.ts
Add partialPrefetching: true to the NextConfig object in next.config.ts to enable Partial Prefetching. This must be done alongside cacheComponents: true.
<Link> prefetch behavior changes with Partial Prefetching
Before Partial Prefetching: <Link href="/x"> prefetched the cached page render, <Link href="/x" prefetch> prefetched the cached page render and any dynamic content, and <Link href="/x" prefetch={false}> disabled prefetching. After Partial Prefetching is enabled: <Link href="/x"> loads the shared App Shell for /x, <Link href="/x" prefetch> loads the App Shell plus URL-specific content through per-link prefetching when /x reads it, and <Link href="/x" prefetch={false}> remains unchanged with prefetching still disabled.
App Shell carries session content from cookies() and headers()
The App Shell includes cached content from cookies() and headers() because these vary per session, not per link. Only params and searchParams are URL data that vary per link and cannot be included in the shared App Shell.
Cached content must have stale time of at least 5 minutes to be included in App Shell
The App Shell carries cached content whose stale time is at least 5 minutes, which holds for the default profile and every preset except seconds. Shorter-lived content streams in after navigation instead.
Auditing <Link prefetch={true}> calls after enabling Partial Prefetching
For fully static or already cached content, remove the now-redundant prefetch={true}. For uncached content you want to keep ahead of click, cache it with use cache then remove prefetch={true}. For content depending on cookies() or headers(), cache the lookup behind the session value then remove prefetch={true}. For content reading URL data or with cached content depending on it, keep prefetch={true} to resolve the content ahead of click. For real-time content that must stay fresh per request, remove prefetch={true} and let the content stream in.
Use 'use cache' directive to cache uncached content for App Shell inclusion
Wrap data fetching with the 'use cache' directive to make uncached content cacheable so it gets included in the App Shell, allowing removal of prefetch={true} from links. Example: async function getProducts() { 'use cache'; const res = await fetch('https://api.example.com/products'); return res.json(); }
Adopt Partial Prefetching incrementally with prefetch = 'partial' per route
With the global partialPrefetching flag still off, use export const prefetch = 'partial' in individual page or layout files to adopt routes incrementally. This scopes the flag to one route and allows auditing and deploying destinations on their own. After auditing all destinations, run the remove-partial-prefetch codemod to remove these exports in one pass.
Use remove-partial-prefetch codemod to clean up incremental adoption
After enabling the global partialPrefetching flag, run npx @next/codemod@canary remove-partial-prefetch ./app to remove all export const prefetch = 'partial' declarations from page and layout files. Use ./src/app for src/ projects. The codemod preserves other prefetch values like prefetch = 'force-disabled'.
Move params and searchParams reads into Suspense boundaries
To ensure the App Shell is not tied to a specific URL, move params and searchParams reads into components wrapped in <Suspense> boundaries. The parent component should pass the params/searchParams promise down without awaiting it, and the child component wrapped in Suspense awaits the promise inside the boundary. This keeps URL-independent parts in the shared App Shell and renders only the URL-specific region per navigation.
Move params read into Suspense boundary example
Before: export default async function Page({ params }: PageProps<'/products/[slug]'>) { const { slug } = await params; const product = await getProduct(slug); return (<ProductLayout><Details product={product} /></ProductLayout>); } After: export default function Page({ params }: PageProps<'/products/[slug]'>) { return (<ProductLayout><Suspense fallback={<DetailsSkeleton />}><ProductDetails params={params} /></Suspense></ProductLayout>); } The child component ProductDetails awaits params inside the Suspense boundary.
Cache URL data with use cache directive for per-link prefetching
To make URL-dependent content resolvable at prefetch time, cache it behind the URL data read using 'use cache' directive. This allows per-link prefetching to work for content depending on params or searchParams. Example: async function getResults(query: string) { 'use cache'; const res = await fetch(`https://api.example.com/search?q=${query}`); return res.json(); }
Use next-partial-prefetching-adoption skill for automated adoption
Install the next-partial-prefetching-adoption skill with npx skills add vercel/next.js --skill next-partial-prefetching-adoption and prompt it with 'Adopt Partial Prefetching in this project using the next-partial-prefetching-adoption skill.' The skill audits <Link prefetch={true}> calls, enables the flag, and sweeps every route for insights.
Opt out routes from instant navigation validation with instant = false
Export export const instant = false from a page or layout to opt that route out of instant-navigation validation if it is not ready to adopt Partial Prefetching. This allows returning to the route later without blocking the build.
Instant navigation insights only appear in development
Development-only insights for URL data outside of Suspense and dynamic data during prefetching never block the build. They appear in the dev overlay with fix cards linking to documentation pages. Load every route in next dev to check for them.
Example: Cache Components product page with Suspense
```tsx
import { Suspense } from 'react'
import { db } from '@/lib/db'
export default function ProductPage(props: PageProps<'/store/[slug]'>) {
return (
<div>
<Suspense fallback={<p>Loading product...</p>}>
<ProductInfo params={props.params} />
</Suspense>
<Suspense fallback={<p>Checking availability...</p>}>
<Inventory params={props.params} />
</Suspense>
</div>
)
}
type Params = PageProps<'/store/[slug]'>['params']
async function ProductInfo({ params }: { params: Params }) {
const { slug } = await params
const product = await getProduct(slug)
return (
<>
<h1>{product.name}</h1>
<p>${product.price}</p>
</>
)
}
async function getProduct(slug: string) {
'use cache'
return db.products.findBySlug(slug)
}
async function Inventory({ params }: { params: Params }) {
const { slug } = await params
const item = await db.inventory.findBySlug(slug)
return <p>{item.count} in stock</p>
}
```
This example shows a product page with cached product info and streamed inventory, each in its own Suspense boundary.
In-memory use cache does not persist across serverless instances
In serverless deployments, in-memory caching with 'use cache' will not persist across instances. Consider using 'use cache: remote' for persistent caching.
Best loading states show cached content and only fallbacks for in-flight data
The best loading states keep as much real, cached content visible as possible and only show fallbacks where data is actually in flight. A Suspense boundary placed high in the tree might satisfy validation but replaces most of the page with a single fallback on every navigation, which feels slower than showing cached content with only targeted fallbacks.
Three key levers for agents optimizing Cache Components routes
Agents working on a Cache Components route typically reach for three levers: Push down (extract I/O into a Suspense-wrapped child so the parent stays static), Cache (pair 'use cache' with cacheLife to assign a freshness profile), and Per-link prefetching (when a route reads URL data, opt it into per-link prefetching so the framework resolves that data at link-prefetch time).
Set instant = false to opt out of validation
Set export const instant = false on a page or layout file to opt the segment out of validation feedback. The segment may still navigate instantly if its structure supports it; the framework just will not surface insights for it. Navigations between sibling segments below are still validated.
use cache: private with stale time >= 5 minutes allows App Shell carry-through
For opted-out segments with content that depends on cookies or headers but has a known cache lifetime, caching it with 'use cache: private' lets the App Shell carry it ahead of the click instead of opting out, as long as its stale time is at least 5 minutes.
Example: Fixing a blocking route with Suspense and cache
```tsx
import { Suspense } from 'react'
async function ProductInfo({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const res = await fetch(`https://next-recipe-api.vercel.dev/products/${slug}`)
const product = await res.json()
return (
<>
<h1>{product.name}</h1>
<p>${product.price}</p>
<p>{product.description}</p>
</>
)
}
async function getFeatured() {
'use cache'
const res = await fetch('https://next-recipe-api.vercel.dev/products?limit=3')
return res.json()
}
export default async function ProductPage(
props: PageProps<'/products/[slug]'>
) {
const featured = await getFeatured()
return (
<div>
<FeaturedSection items={featured} />
<Suspense fallback={<p>Loading product...</p>}>
<ProductInfo params={props.params} />
</Suspense>
</div>
)
}
```
This shows the pattern to fix a blocking route: wrap slug-dependent work in Suspense and cache the featured list.
What instant navigation means
A navigation is instant when the browser can start rendering the new page the moment the user clicks, with static, cached, and fallback content showing up right away, while the server streams the remaining content into its fallbacks. This definition assumes caches are warm; cold caches still require the server to compute the cached result once, so the first navigation to a route may still wait.
Direct visits vs client navigations produce different initial UI
Direct visits get the static shell as HTML, typically from a CDN. Client navigations only re-render below the layout the current and destination routes share, so the fallback UI defined by a Suspense boundary above that point cannot be used during the transition. This is why the same route can show different initial UI depending on how it is reached.
Enable Cache Components and Partial Prefetching for instant navigation
Set cacheComponents: true and partialPrefetching: true in next.config.ts to enable the features required for instant navigation.
use cache directive assigns lifetime to async function results
Caching directives ('use cache' and its variants) assign a lifetime to an async function's result, which allows Next.js to include it in the static shell. The 'use cache' directive enables a function's cached result to be part of the static shell that is included in the initial render.
use cache: private caches only in browser, not in static shell
'use cache: private' is a variant for caching functions that read runtime APIs like cookies() and headers(). The result is cached in the browser only, not on the server. It cannot be part of the static shell.