In modern web applications, distinct pages have fundamentally divergent requirements regarding delivery latency, data freshness, and infrastructural load:
Next.js App Router consolidates these requirements into a cohesive hybrid rendering system. Instead of the legacy page-level demarcation (getServerSideProps vs getStaticProps in Pages Router), modern Next.js configures rendering strategies declaratively at the Route Segment level and per individual data query (Data Cache).
Next.js App Router provides three primary rendering architectures:
cookies(), headers(), searchParams) or explicitly via export const dynamic = 'force-dynamic'.Client A Edge CDN Background Worker (Node.js)
│ │ │
(1) ├──── GET /product/42 ────────►│ │
│ │ [Cache Fresh: age < 60s] │
│◄─── 200 OK (HTML/RSC) ───────┤ │
│ │ │
═══════╪══════════════════════════════╪══════════════════════════════════╪═══════
│ (65 seconds elapse: Cache transitions to STALE) │
│ │ │
(2) │ Client B │ │
├──── GET /product/42 ────────►│ │
│ │ [Cache Stale! Return stale copy] │
│◄─── 200 OK (STALE copy) ─────┤ │
│ │ │
│ ├── Async render trigger ─────────►│
│ │ ├── Query Database
│ │ ├── Compile HTML/RSC
│ │◄── New HTML/RSC Payload ─────────┤
│ │ [Cache Updated: age = 0s] │
│ │ │
(3) │ Client C │ │
├──── GET /product/42 ────────►│ │
│ │ [Cache Fresh!] │
│◄─── 200 OK (FRESH payload) ──┤ │The page compiles during build (SSG) or on-demand upon first visit. Generated HTML and RSC Flight artifacts are saved to Full Route Cache and Edge CDN.
For the duration of the configured interval (e.g., revalidate = 60), all users receive instant CDN cache hits with sub-50ms TTFB.
Once 60 seconds pass, the cache is marked Stale. The next request immediately receives the stale copy without awaiting the server.
Next.js asynchronously spawns a background worker to re-evaluate page code, query databases, and regenerate artifacts.
Upon successful compilation, the new HTML and RSC payloads atomically replace the cached entries for all subsequent requests.
What happens when a user requests a page configured with revalidate = 60 seconds exactly 65 seconds after its previous generation?
Route behaviors can be explicitly declared by exporting segment configuration options from page.tsx or layout.tsx:
// 1. Force Dynamic SSR (disables page caching completely)
export const dynamic = 'force-dynamic';
// 2. Force Static SSG (dynamic functions return empty stubs)
// export const dynamic = 'force-static';
// 3. Time-based ISR: revalidate the page at most once every 60 seconds
// export const revalidate = 60;
// 4. Behavior for params not generated in generateStaticParams:
// true: generate on-demand on first request and cache via ISR (default)
// false: return 404 Not Found
export const dynamicParams = true;
export default async function DashboardPage() {
return <div>Dynamic Control Panel</div>;
}[!IMPORTANT] Architectural Standard: Beginning with Next.js 15, native
fetch()calls default to uncached execution (cache: 'no-store'). This is the exact inverse of Next.js 13/14 behavior!
To govern individual network call caching, pass options inside the fetch configuration argument:
// 1. SSG: Permanent Data Caching
const staticData = await fetch('https://api.example.com/config', {
cache: 'force-cache'
});
// 2. Time-based ISR: Cache with timed revalidation (1 hour)
const timedData = await fetch('https://api.example.com/products', {
next: {
revalidate: 3600,
tags: ['products-list'] // Cache tag for on-demand purging
}
});
// 3. SSR: Uncached request (Next.js 15+ default behavior)
const liveData = await fetch('https://api.example.com/notifications', {
cache: 'no-store'
});revalidateTag and revalidatePath#Event-driven cache invalidation (such as catalog updates from a CMS webhook or admin panel action) executes on the server via Server Actions or Route Handlers:
'use server';
import { revalidateTag, revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
interface UpdateProductDTO {
title: string;
price: number;
}
export async function updateProduct(id: string, data: UpdateProductDTO): Promise<{ success: boolean }> {
// 1. Mutate record in database
await db.product.update({
where: { id },
data
});
// 2. Invalidate specific cache tag (targets matching Data Cache entries)
revalidateTag(`product-${id}`);
// 3. Invalidate catalog page layout cache
revalidatePath('/catalog', 'page');
return { success: true };
}unstable_cache#When reading data directly via ORMs (Prisma, Drizzle, TypeORM), native fetch is absent. To bind database reads into the Next.js Data Cache and ISR system, wrap the queries using unstable_cache:
import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';
// Wrap database query in the Next.js Data Cache layer
export const getCachedPost = unstable_cache(
async (slug: string) => {
return await db.post.findUnique({
where: { slug }
});
},
['post-by-slug'], // Key parts for cache differentiation
{
revalidate: 300, // ISR: 5 minutes
tags: ['posts'] // Target tag for on-demand invalidation via revalidateTag('posts')
}
);For e-commerce catalogs with 200,000 SKUs, pre-rendering every page during CI/CD is wasteful. This pattern pairs generateStaticParams for the top 100 products with automatic background ISR for the rest:
import { notFound } from 'next/navigation';
import { db } from '@/lib/db';
interface PageProps {
params: Promise<{ id: string }>; // In Next.js 15+, access is strictly asynchronous
}
// 1. Pre-render top-100 popular products at build time
export async function generateStaticParams() {
const popularProducts = await db.product.findMany({
where: { isPopular: true },
take: 100,
select: { id: true }
});
return popularProducts.map((p) => ({ id: p.id }));
}
// 2. Allow on-demand generation for the remaining 199,900 SKUs upon first visit
export const dynamicParams = true;
// 3. Revalidate background cache at most once every 10 minutes
export const revalidate = 600;
export default async function ProductPage({ params }: PageProps) {
const { id } = await params;
const product = await db.product.findUnique({ where: { id } });
if (!product) {
notFound();
}
return (
<article className="p-8 max-w-3xl mx-auto">
<h1 className="text-3xl font-bold">{product.title}</h1>
<p className="text-2xl font-mono text-accent-cyan mt-2">${product.price}</p>
<div className="mt-4 text-text-muted leading-relaxed">{product.description}</div>
</article>
);
}import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag } from 'next/cache';
export async function POST(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret');
// Verify authorization secret
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ message: 'Invalid token' }, { status: 401 });
}
const body = (await request.json()) as { tag?: string };
if (!body.tag) {
return NextResponse.json({ message: 'Tag missing' }, { status: 400 });
}
// Instantly invalidate Data Cache for all requests with matching tag
revalidateTag(body.tag);
return NextResponse.json({ revalidated: true, tag: body.tag, now: Date.now() });
}Which strategy most efficiently updates a recommended products widget without triggering a full rebuild of the entire catalog page?
In Next.js 13 and 14, all fetch() calls were cached aggressively under force-cache. Developers frequently assumed static fetching was the default.
In Next.js 15+, the contract is reversed: fetch() defaults to cache: 'no-store'. Calling await fetch('https://api.com/items') without explicit cache settings switches the page to Dynamic SSR, hitting the external API on every single visit!
Mitigation: Always declare explicit caching options (force-cache for SSG or next: { revalidate: 60 } for ISR).
// For SSG:
const res = await fetch(url, { cache: 'force-cache' });
// For ISR:
const res = await fetch(url, { next: { revalidate: 60 } });Calling await cookies() or await headers() in any Server Component (even deeply nested in a sidebar) forces Next.js to de-opt the entire route into Dynamic SSR.
Mitigation:
<Suspense fallback={<CartSkeleton />}>—the page shell remains SSG/ISR, while the dynamic slot streams independently.// components/CartBadge.tsx (Server Component in layout sidebar)
import { cookies } from 'next/headers';
export async function CartBadge() {
const cookieStore = await cookies(); // ❌ De-opts entire route to SSR without Suspense!
const cartId = cookieStore.get('cartId')?.value;
return <span>{cartId ? 'Items: 1' : 'Empty Cart'}</span>;
}When revalidate: 60 expires, the first visitor triggers a background page rebuild. If compilation takes 3–5 seconds and traffic is running at 500 RPS, absence of distributed locks can lead to dozens of concurrent workers overwhelming databases with duplicate queries.
Mitigation: While Next.js provides built-in deduplication for route revalidation, pair direct database ORM calls with unstable_cache tags and rely on on-demand event-based invalidation (revalidateTag) via webhooks rather than aggressive short-interval polling.
Calling revalidatePath('/') clears the cache solely for the index page. To flush the entire application tree, one must specify 'layout': revalidatePath('/', 'layout'). However, overusing this in production causes sudden database traffic spikes as thousands of pages expire concurrently. Prefer granular revalidateTag().