D
P
0

Next.js

Route Only 500s in Production, `pnpm dev` Is Fine? `DYNAMIC_SERVER_USAGE` From an Empty `generateStaticParams` That Reads `searchParams`

July 31, 2026·4 min read
Route Only 500s in Production, `pnpm dev` Is Fine? `DYNAMIC_SERVER_USAGE` From an Empty `generateStaticParams` That Reads `searchParams`

A client site built on Next.js 16 had coin detail pages at /coins/[slug]. Every coin got its own page: price, stats, and a chart with a range switcher, buttons for 24h, 7d, 30d. Locally everything was smooth. I ran pnpm dev, opened /coins/bitcoin, and got a 200 with a full render. I pushed to Railway, the build succeeded, I opened the exact same production URL, and got Internal Server Error. HTTP 500. Not once, but consistently, and always after about five seconds of loading.

What stood out: it was not one coin. Every /coins/[slug] route 500'd in production. Meanwhile every other route, the homepage, the list page, the about page, stayed 200 and healthy. So something specific to this coin detail template was going down, and only in a production build.

The first dead end

My first guess was wrong, and I burned time on it. Since the detail page reads a lot of fields from an API, I suspected a null deref. One suspicious field was athDate, the all-time-high date that is sometimes missing for new coins. I figured the render was blowing up on a property access against null, so I patched it with optional chaining everywhere.

const ath = coin.athDate?.toLocaleDateString();

Pushed again. Still 500. Of course, because that null deref was never actually happening. If there were a real TypeError, dev would blow up too, and dev was clean. I was shooting at the wrong symptom because I had not read the actual error yet. The classic lesson: do not patch before you know the error.

Reading the real error

Since dev was green and only production was red, this was clearly the dev-and-build-behave-differently class of bug. Next.js dev mode is permissive; it renders on demand and does not run the static-optimization path that next build uses. So I stopped guessing and reproduced production conditions on my own machine:

pnpm build && pnpm start

The 500 surfaced immediately in local. Now I could read the real stderr instead of guessing from the client error page. The digest was blunt:

Error: digest 'DYNAMIC_SERVER_USAGE'

That was the clue. A dynamic API was being touched at the exact moment Next.js was trying to statically optimize this route.

Root cause: searchParams plus an empty generateStaticParams

The coin detail route had two things that, on their own, looked harmless.

First, it declared a generateStaticParams() that returned an empty array:

export async function generateStaticParams() {
  return [];
}

My intent: do not pre-render any coin at build, render everything on demand. But returning an empty array is not the same as turning off static optimization. Next.js 16 still treats the route as a static candidate.

Second, the page read searchParams to know which chart range was active:

export default async function CoinPage({ searchParams }) {
  const range = searchParams.range ?? "24h";
  const chart = await getCoinChart(slug, range);
  // ...
}

searchParams is a dynamic API. Reading it marks the render as request-dependent. And there is the conflict: Next.js tries to statically optimize the route because generateStaticParams is empty, but the render touches searchParams, which demands a request context. Static versus dynamic collide, and that collision explodes into DYNAMIC_SERVER_USAGE at request time, in production builds only.

Worth flagging: this is different from a case I wrote up earlier where the dynamic API was a draftMode() buried deep in a nested component fetch chain. Here nothing is nested. searchParams is read right at the top of the page. Precisely because it looks ordinary, it is easy to miss.

Dev never surfaced this because dev always renders in a dynamic context, so searchParams always has a request to lean on. That is why local was green and production was red for the exact same code.

The fix

One line. Drop the empty generateStaticParams and replace it with an explicit dynamic declaration:

export const dynamic = "force-dynamic";

This tells Next.js to never attempt to statically optimize this route. No more request-less render, so searchParams always has a valid context. The 500 was gone, and the chart range buttons worked as they should.

Takeaways

  • A generateStaticParams that returns an empty array is not how you turn off static optimization. Next.js 16 still treats the route as a static candidate.
  • An empty generateStaticParams is safe ONLY when the render touches params alone. The moment it reads searchParams, cookies(), headers(), or draftMode(), you need force-dynamic.
  • Do not patch before reading the error. The optional chaining on athDate was a dead end because the null deref never happened.
  • Reproduce production with pnpm build && pnpm start. pnpm dev hides an entire class of static-versus-dynamic bugs.
  • When production 500s consistently and the error persists, read the real stderr. The DYNAMIC_SERVER_USAGE digest cut hours of guessing.