Next.js Data Fetching & Caching: fetch, ISR, and Revalidation
Understand how the Next.js App Router really fetches and caches: fetch defaults, static vs dynamic rendering, ISR with revalidate, generateStaticParams, and tag-based revalidation.
Prerequisites
- •Part 1 of this series (App Router basics)
- •Familiarity with async/await and HTTP
What You'll Learn
- Fetch data in server components with correct cache options
- Distinguish static, dynamic, and ISR rendering — and control each
- Pre-render dynamic routes with generateStaticParams
- Invalidate cached data on demand with revalidatePath and revalidateTag
Next.js App Router Essentials
Data Belongs in Server Components
The App Router's data story starts simply: fetch where you render.
export default async function ReleasesPage() {
const res = await fetch("https://api.github.com/repos/vercel/next.js/releases");
const releases = await res.json();
return <ReleaseList releases={releases} />;
}No client-side waterfalls, no exposed API keys, no loading spinners for first paint. The interesting question is when this code runs — and that's what caching controls.
fetch and Its Cache Options
In current Next.js, fetch in server components is not cached by default — each request hits the network unless you opt in:
// Cache indefinitely (until manually revalidated) — static data
await fetch(url, { cache: "force-cache" });// Cache, but refresh in the background at most every hour — ISR per-request
await fetch(url, { next: { revalidate: 3600 } });
// Never cache — always fresh
await fetch(url, { cache: "no-store" });
// Cache and label it for targeted invalidation
await fetch(url, { next: { tags: ["releases"] } });
For non-fetch data sources (database clients, SDKs), the same ideas apply at the route level, or via React's cache() for request-level deduplication.
The Three Rendering Modes
Every route ends up in one of three modes:
Static (build time)
If a route uses no dynamic APIs (cookies(), headers(), searchParams) and no uncached data, Next.js renders it once at build and serves pure HTML from then on. Fastest possible response; content frozen until the next deploy.
Dynamic (request time)
Touch a dynamic API or uncached data, and the route renders per request. Personalization and always-fresh data — at per-request server cost. You can force it explicitly:
export const dynamic = "force-dynamic";ISR — the Middle Path
Incremental Static Regeneration serves static HTML and keeps it fresh. One line opts a whole route in:
export const revalidate = 300; // regenerate in background at most every 5 minThe behavior to internalize: after 300 seconds, the next visitor still gets the old page instantly while regeneration runs in the background; the visitor after that gets the fresh one. Stale-while-revalidate, built in. This is the sweet spot for content sites: product listings, blog posts, docs — static speed, hourly-or-better freshness, and it degrades gracefully if a regeneration fails (the last good page keeps serving).
Pre-rendering Dynamic Routes
Dynamic segments render on demand by default. generateStaticParams tells Next.js which ones to build ahead of time:
// app/blog/[slug]/page.tsx
export const revalidate = 3600;export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((p) => ({ slug: p.slug }));
}
Known posts become static pages at build; unknown slugs render on first request and are then cached — the classic pattern for blogs and catalogs.
On-Demand Revalidation
Time-based freshness is a guess. When you know data changed — a CMS webhook fired, an admin hit save — invalidate precisely:
"use server";
import { revalidatePath, revalidateTag } from "next/cache";export async function publishPost(slug: string) {
await db.post.publish(slug);
revalidatePath("/blog/" + slug); // this page
revalidateTag("posts"); // every fetch tagged "posts"
}
Tags are the scalable version: tag your fetches by entity (posts, product:42), then invalidate the tag wherever mutations happen. The next request re-fetches only what was tagged.
A Decision Cheat Sheet
revalidate + generateStaticParamscookies() makes it automatic)cache: "no-store" fetchesDebugging Cache Behavior
Two habits save hours: run next build and read the route table — it marks each route static, ISR (revalidate column), or dynamic — and when a page "won't update", check in order: fetch cache option → route revalidate → whether a dynamic API silently made everything dynamic.
Next: writing data back — Server Actions, forms, and optimistic UI — in Part 3: Server Actions & Forms.

