A Next.js catalog serves 50,000 products from app/products/[id]/page.tsx, but generateStaticParams returns only the 500 most popular ids. What does a visitor get when they request one of the other 49,500, and what changes if the segment also exports dynamicParams = false?
answer
- a build list, not an allow-list
- what happens to the unlisted 49,500
- one segment export decides it
- open set versus closed set
- no server means nothing to render on demand
basics
~20 sWith the default dynamicParams = true, an id missing from generateStaticParams is still served: Next renders that page on demand at request time. Exporting dynamicParams = false instead makes any unlisted id return the not-found response.
solid answer
~50 s`generateStaticParams` is a prerender list, not an allow-list. By default (`dynamicParams = true`) the 500 listed ids are built at build time and the remaining 49,500 are rendered **on demand** when someone requests them — the first visitor pays the render cost, and whether later visitors reuse that output depends on the segment's revalidation settings and the incremental cache on your host. Exporting `export const dynamicParams = false` flips it to a closed set: anything not in the list returns the not-found response instead of rendering. Which you want depends on the segment. For a large catalog where ids are legitimate but too numerous to build, keep the default — fast prerendered pages for the head of the distribution, correctness for the long tail. Set it to `false` when the enumerated list genuinely is the whole world, or when you produce a fully static export with no server at request time, since nothing can render an unlisted path.
code
typescript · 13 lines// app/products/[id]/page.tsx
// Default: unlisted ids are still rendered on demand.
// export const dynamicParams = false // <- unlisted ids would 404 instead
async function getTopProductIds(limit: number): Promise<number[]> {
const res = await fetch(`https://example.com/api/top-products?limit=${limit}`)
return res.json()
}
export async function generateStaticParams() {
const ids = await getTopProductIds(500)
return ids.map((id) => ({ id: String(id) }))
}go deeper
Know that generateStaticParams lists the paths built ahead of time and that leaving a path off it does not, by itself, make that URL invalid.
Explain both settings precisely: the default renders unlisted paths on demand, while dynamicParams = false turns the enumerated list into a closed set that returns not-found for everything else.
Reason about the whole catalog: which slice is worth building, what the first visitor to a long-tail page experiences, how the built pages stay fresh, and why a fully static export forces the closed-set answer.
Own the build-time budget as a shared cost. Set the policy for when a team may enumerate a large set, how content launches avoid silent 404s, and how freshness guarantees are agreed with the teams that own the underlying data.
## The mental model The single most useful sentence here: **`generateStaticParams` is a build list, not a route guard.** Returning an id means "build this page now". Omitting one does not mean "this URL does not exist" — that is a separate decision, made by the `dynamicParams` segment export. ## Default behaviour: dynamicParams = true This is the default, so it applies unless you say otherwise: - The 500 listed ids are rendered during the build and served from that prerendered output. - A request for id `98123`, which was never listed, is **rendered on demand**. The visitor gets a real page, produced at request time on the server. What happens to that on-demand result afterwards is the part to be careful about in an interview, because it depends on configuration and on where you deploy. If the segment has a revalidation window configured, the rendered output can be stored in the incremental cache and reused for later visitors until it expires; if the page opted into per-request rendering, it is produced fresh every time. Rather than assert a universal default here, say what it depends on — that is the answer of someone who has actually operated one of these. The user-visible consequence is a latency split: head-of-catalog pages are instant, long-tail pages cost a render on first hit. For a 50,000-product catalog with a heavy power-law traffic distribution, that is usually the right trade — you spend build time only where it buys you something. ## dynamicParams = false ```ts // app/products/[id]/page.tsx export const dynamicParams = false export async function generateStaticParams() { const ids = await getTopProductIds(500) return ids.map((id) => ({ id: String(id) })) } ``` Now the enumerated list is the complete set of valid URLs. A request for any other id returns the not-found response rather than being rendered. In the scenario as posed that is a bug: 49,500 real products would become unreachable. It is the right setting only when the list truly is exhaustive — a docs site whose pages all come from files in the repo, a marketing site with a fixed set of landing pages, or a set of legacy redirect targets. ## The static-export case If you build the app as a fully static export, there is no server at request time to perform an on-demand render. In that world an unlisted path cannot be produced at all, so the enumerated list has to be complete and the closed-set behaviour is the only coherent one. This is a good thing to raise unprompted: the choice between the two settings is partly a deployment-shape question, not only a routing one. ## Real failures teams hit - **Silent 404s after a content launch.** A team sets `dynamicParams = false` while the catalog is small and enumerable, the catalog grows, and newly created products 404 until the next deploy. Nothing errors; traffic just quietly fails. - **Build-time explosion the other way.** Someone "fixes" a slow long-tail page by enumerating all 50,000 ids, and the build goes from four minutes to over an hour, with every deploy paying it. - **A stale head.** The prerendered top 500 are built once and never revalidated, so price or stock changes never appear on exactly the pages with the most traffic. Enumerating a path decides *when* it renders; it does not exempt you from deciding how it stays fresh. - **Unbounded ids as an amplification surface.** With on-demand rendering, every distinct fabricated id in the address bar is a fresh server render. A cheap crawler can walk `/products/1` upward forever. Validate the id shape and reject nonsense early rather than letting arbitrary input reach the data layer. ## How to decide, in one breath Ask whether the set of valid values is **closed and small enough to build**. If yes — repo-backed docs, a fixed set of landing pages, a static export — enumerate it fully and set `dynamicParams = false` so typos become honest 404s. If the set is open or large — a product catalog, user-generated content — enumerate the valuable head, leave the default in place so the tail still works, and put your effort into the revalidation story for the pages you did build.
- So is generateStaticParams a security or validation boundary for the id?No. With the default setting any value reaches your page, and even with `dynamicParams = false` the check is only "was this string in the build list" — it says nothing about authorization. Validate the id's shape in the page and enforce access control in the data layer; routing decides which renderer runs, never who may see the result.
- How would you keep the prerendered top 500 pages from going stale on price changes?Enumerating a path fixes when it first renders, not how it stays current. You still need a revalidation strategy — a time window on the segment, or on-demand invalidation triggered by the system that owns the price — otherwise the highest-traffic pages are the ones frozen at build time. Decide this at the same moment you decide the list.
- What would push you to enumerate all 50,000 ids instead of 500?A hard requirement that no visitor ever pays a cold render — an SEO landing surface, or a host where on-demand rendering is unavailable — plus a build budget that can absorb it. Measure it: 50,000 renders at even 50 ms each is over 40 minutes of build time on every deploy, paid by every engineer who ships.
saying these in an interview costs you the question
- Thinks unlisted params 404 by default
- Calls generateStaticParams an allow-list of valid ids
- Sets dynamicParams = false on an open-ended catalog
- Assumes prerendered pages refresh themselves over time
- Proposes enumerating all 50,000 ids without a build budget