Skip to content

NEXT.JS

Next.js generateStaticParams and dynamicParams: What 404s

How generateStaticParams and dynamicParams decide which dynamic routes exist in Next.js 15, with our build manifest, the stale-build 404 we hit, and when to pick each setting.

generateStaticParams returns the list of dynamic route values Next.js prerenders at build time, and dynamicParams decides what happens to every value not on that list. With dynamicParams = false, an unlisted value returns 404; with the default true, it renders on first request. Setting false means a new page does not exist until you rebuild.

We checked both settings against this site's own production build on next@15.5.20, then served that build with next start to see what a visitor gets.

Key takeaways

  • dynamicParams = false compiles to fallback: false in the prerender manifest; the default compiles to fallback: null.
  • A server running an old build returns 404 for content published after it, with no fallback render.
  • The param list should come from the same loader the page reads, never a second hand-kept list.
  • generateStaticParams runs at build time, not per request.

What generateStaticParams does

generateStaticParams is an exported function on a dynamic route ([slug], [category]/[slug]) that returns an array of param objects. Next.js renders one static page per object when you run next build.

Our article route returns every published article's category and slug:

app/[category]/[slug]/page.tsx (excerpt)
import { getArticleParams } from "@/lib/mdx";

/** Every published article is prerendered; unknown routes 404. */
export async function generateStaticParams() {
  return getArticleParams();
}

export const dynamicParams = false;

getArticleParams() walks content/<category>/<slug>.mdx, skips drafts, and maps each file to { category, slug }. The path is the route, so the param list cannot disagree with the files on disk.

The result in the last build is plain files in .next/server/app, such as legal/cookies.html, which the host serves without running the page.

dynamicParams true versus false

dynamicParams is a route segment config with two values, and the build manifest records which one each route compiled to. We read both from .next/prerender-manifest.json:

RouteSourceManifest fallbackUnlisted value
/[category]/[slug]dynamicParams = falsefalse404
/legal/[slug]default (true)nullRenders on request

Both routes call generateStaticParams. The difference is only the unlisted case. Our /legal/[slug] page still returns 404 for /legal/not-a-policy, but through its own notFound() call after the lookup fails, not through the framework.

That distinction matters for crawlers. With false, a mistyped URL costs no render and the 404 is immediate. With true, each unknown value runs your page code once, so the page must handle a missing record itself.

The stale build 404

With dynamicParams = false, the set of pages is frozen at build time, so content added afterward 404s until you rebuild. We hit this while checking which articles existed on 26 October. The count includes this article, which was written after the build.

The .next directory held a build from 24 October 20:36. Comparing its manifest with the content tree:

Terminal
npm run check:static-params
# → build prerendered 169 of 173 article routes; 4 newer:
#   nextjs-after-background-work, nextjs-generatestaticparams-dynamicparams,
#   nextjs-middleware-matcher, nextjs-route-handlers

After npm run build the same check reports 173 of 173, so the gap was staleness and nothing else. Before rebuilding, we served the old build and requested one old and one newer article:

Terminal
npx next start --port 3417
curl -s -o /dev/null -w "%{http_code}" localhost:3417/next-js/nextjs-unstable-cache
# → 200
curl -s -o /dev/null -w "%{http_code}" localhost:3417/next-js/nextjs-route-handlers
# → 404

The second file exists in content/ and renders in next dev, yet the old build has no HTML for it. The server log recorded Error: Internal: NoFallbackError for the missing page. Nothing is broken in the page code; the build is simply older than the content.

This is the cost of dynamicParams = false: publishing is a deploy. Our pipeline already treats it that way, because every article is committed and built.

One source for the param list

The param list and the page body must read the same data source. If generateStaticParams reads a hand-kept array while the page reads files, you get two ways to drift: a slug in the array with no file renders a broken page, and a file with no entry 404s.

app/blog/[slug]/page.tsx
import { listPosts } from "@/lib/posts"; // your own loader

type PageProps = { params: Promise<{ slug: string }> };

export async function generateStaticParams() {
  const posts = await listPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function Page({ params }: PageProps) {
  const { slug } = await params;
  return <h1>{slug}</h1>;
}

In Next.js 15 the page receives params as a Promise, so you await it. The values generateStaticParams returns are plain strings, not Promises. See Next.js 15 async request APIs for the migration.

If the loader reads files or a database, cache the parse so it runs once per process rather than once per page.

Which setting to pick

Pick the setting by who controls the list and when it changes.

  • dynamicParams = false: the list is complete at build time and publishing already triggers a build. Content sites, docs, and changelogs fit. You get immediate 404s and a fixed page set.
  • Default true with notFound(): the data changes between deploys, such as a product catalog edited in a dashboard. Unknown ids render on demand, so the page looks the record up and calls notFound() when it is absent. See error.tsx versus not-found.tsx.
  • generateStaticParams returning []: prerender nothing at build and render everything on demand. Use it when the list is too large to build, not as a default.

Best practices

  1. Derive params from the page's own loader. One source cannot disagree with itself.
  2. Keep generateStaticParams free of request data. It runs at build time and has no cookies or headers.
  3. Treat publishing as a build when dynamicParams is false. Add a rebuild step to the content workflow.
  4. Test the 404 deliberately. Request one valid and one invalid URL against next start before shipping.
  5. Record the build you served. Compare the manifest with the content tree after each deploy.

Common mistakes

  • Serving an old .next after adding content. Symptom: new pages 404 on next start but work in next dev. Fix: run the build again.
  • Expecting dynamicParams = false to hide drafts. It only restricts the route to the list you return. Filter drafts in the loader.
  • Reading request data in generateStaticParams. It runs at build time, so there is no request. Fix: move the lookup into the page.
  • Hand-writing a second slug list. Symptom: a slug in the list renders an empty page. Fix: map over the real data.

Conclusion

Return the full list from generateStaticParams, set dynamicParams = false when publishing already means a deploy, and keep the default with notFound() for data that changes between deploys. After any content change, rebuild before you judge a 404: our build was two days old and four articles short.

For the rest of the routing surface, read Next.js 15 Partial Prerendering and the Route Handlers guide.

Frequently asked questions

What does generateStaticParams do in Next.js?
generateStaticParams returns the list of dynamic segment values, such as every blog slug, that Next.js prerenders to static HTML at build time. Without it, a dynamic route renders on demand. With it, the listed pages ship as files, which is why they load from the CDN without running your page code.
What does dynamicParams = false do?
dynamicParams = false makes any param not returned by generateStaticParams respond with a 404 instead of rendering on demand. In our build manifest the route compiles to fallback: false. Leaving it at the default true compiles to fallback: null, and unknown values render when first requested.
Why does my new page 404 after I add content?
If dynamicParams is false, the page set is fixed when you build. A server still running the previous build has no HTML for your new slug and returns 404 until you rebuild and redeploy. We reproduced this with next start on a build that predated four articles.
Is generateStaticParams run on every request?
No. It runs at build time for the static params, and again only when a build or revalidation regenerates the route. Request time never calls it. That is why it must not depend on cookies or headers, and why changes to its data source need a new build to take effect.
Should I use dynamicParams false or notFound()?
Use dynamicParams = false when the full list is known at build time and a rebuild is part of publishing. Use the default plus notFound() when the data changes between deploys, such as a database of products, and a missing record should 404 after a lookup.

Muhammad Kashif

Founder and editor of Devventa, covering AI coding assistants, Next.js and the modern AI development stack.