Lesson 78 of 81
Four plain exported variables, each overriding a different layer of a route's caching and rendering behavior — and each one's default already covers most routes, which is exactly why the override options are so rarely needed and so easy to misuse when they are.
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A page, layout, or route handler can export a small set of plain variables — not functions, just assigned constants — that override how that segment is rendered and cached. These are separate knobs, each controlling a different layer, and the confusion usually comes from not knowing which layer a given bug actually lives in:
dynamic controls the segment's overall rendering strategy: 'auto' (the default — cache what can be cached, render dynamically whatever can't), 'force-dynamic' (always render per-request, no matter what), 'force-static' (always prerender, forcing request-time APIs like cookies() to read as empty rather than making the route dynamic), or 'error' (prerender, and throw a build error if anything inside tries to use request-time data).revalidate sets a default time-based cache lifetime, in seconds, for the segment as a whole. false (the default) means cache indefinitely unless something forces it dynamic; 0 means always dynamic; a positive number sets a revalidation window — but only as a ceiling default, since an individual fetch call's own revalidate option always takes precedence over the segment's.fetchCache is the escape hatch for the rare case where you need to override the default caching behavior of fetch itself across the whole segment, rather than per-call. Its own reference docs call it out explicitly as advanced — reach for dynamic or a per-fetch cache option first, almost always.runtime picks the JavaScript execution environment (covered in depth elsewhere in this track) — 'nodejs' by default, with 'edge' now deprecated.// app/blog/[slug]/page.tsx — mostly static, occasional content updates
export const revalidate = 3600; // revalidate at most once per hour
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug); // inherits the segment's revalidate window
return <Article post={post} />;
}
// app/dashboard/page.tsx — must reflect this exact moment, every time
export const dynamic = "force-dynamic";
export default async function Dashboard() {
const stats = await getLiveStats(); // re-runs on every request, unconditionally
return <Stats stats={stats} />;
}
// app/api/build-info/route.ts — must be computed once and never diverge per request
export const dynamic = "error";
export async function GET() {
// If anything in here ever touches cookies(), headers(), or an uncached
// fetch, the BUILD fails loudly instead of silently becoming dynamic.
return Response.json({ buildId: process.env.BUILD_ID });
}
The blog post only sets revalidate — it never touches dynamic at all, because 'auto' already does the right thing once a revalidation window exists. The dashboard needs force-dynamic specifically because per-request freshness is the whole point, not an optimization being fought against. The build-info route uses 'error' as a tripwire: if a future edit accidentally introduces something request-specific, the build catches it immediately instead of the route quietly starting to render dynamically in production with nobody noticing the behavior changed.
revalidate is a default, not a floor or a ceiling that overrides individual fetches. A fetch call with its own explicit revalidate option always wins over whatever the segment exports — the segment-level setting only fills in for fetches that didn't specify one themselves.force-static doesn't just skip rendering dynamically — it makes request-time APIs lie. cookies(), headers(), and useSearchParams() all return empty values under force-static, by design, so the route can still be fully prerendered. Code that assumes it can read a real cookie under this setting is wrong, not broken.fetchCache is explicitly documented as an advanced, rarely-needed override — reaching for it before trying dynamic or a per-fetch cache option is usually solving the wrong layer of the problem, and its own interaction rules across nested segments (which combinations of its values are even allowed to coexist in one route) are intricate enough that most teams never need to learn them.dynamic/fetchCache values across nested segments of the same route is a real, enforced error, not just bad practice — 'force-cache'-equivalent and 'force-no-store'-equivalent settings on different segments of one route tree can't coexist, because the route has to resolve to one coherent rendering strategy.export const revalidate = isProd ? 3600 : 0 doesn't work the way it looks like it should — Next.js reads these at build time via static analysis, not by executing your module's top-level logic."A dashboard page needs to be real-time, but the blog pages on the same site should stay static and fast" tests whether a candidate knows these are per-segment settings, not a single site-wide switch — and whether they reach for revalidate on the content that's mostly static but occasionally stale-tolerant, versus force-dynamic only on the page that genuinely can't tolerate any staleness at all, rather than reflexively forcing the whole site dynamic "to be safe."