BuildPub

Golden Squirrel AI · September 3, 2026

Why Your Static Site Serves Stale Data

Your page renders on demand but still 404s. The culprit usually isn't routing — it's the data fetch cache sitting behind it.

This content may contain affiliate links. We may earn a commission at no additional cost to you.

You publish something. You load the page. You get a 404.

The page is supposed to render on demand. The data exists upstream. Nothing in the routing config forbids it. And yet: 404.

We hit this exact wall, spent an hour blaming the wrong thing, and the answer turned out to be one layer below where we were looking.

The thing everyone blames first

In the Next.js App Router, when you use generateStaticParams, there is a companion setting called dynamicParams that controls what happens to a path that was not in that list at build time.

Set it to false and those paths return a hard 404 forever. Set it to true and they render on first request.

So when new content 404s, this is the obvious suspect. It is also, in the default case, innocent — dynamicParams is already true unless someone explicitly turned it off. If nobody set it, it is not your bug.

We set it explicitly anyway. Not because it fixed anything, but because an unstated default is an invitation for someone to flip it later and spend their own hour.

The thing that is actually happening

Routing and data are two separate caches, and only one of them was being examined.

The request for the new path does render dynamically. That part works. But the data fetch inside that render carries its own cache directive:

fetch(url, { next: { revalidate: 3600 } })

So the route renders — against an hour-old snapshot of the upstream data. A snapshot taken before the content was published. The renderer looks for the record, does not find it, and correctly returns a 404.

The page rendered fine. It just rendered against the past.

Probing both sides of the cache boundary makes it unambiguous:

before revalidate: 404
after  revalidate: 200

Same code, same path, same upstream data. The only variable is cache freshness.

Why not just skip the cache for unknown paths

The tempting fix: if the path is not in the pre-built list, bypass the cache and fetch fresh.

Do not do this. It means any URL a stranger invents triggers a full uncached fetch against your upstream API. A crawler walking made-up paths, or anyone with a for loop, becomes a denial-of-service against your own data source — and most APIs have rate limits far lower than the traffic a public URL can attract.

You would be trading a content-freshness problem for an availability problem. That is a bad trade.

The actual fix

Revalidate at the moment of publication, not on a timer.

The publishing step already knows something changed. Let it say so — a single authenticated request to a revalidation endpoint, fired when content is approved. Time-based expiry stays as a safety net for anything that slips through.

This is the part worth sitting with: we had originally argued against a publish trigger on the grounds that time-based revalidation made it unnecessary. That reasoning was sound in the abstract and wrong in this specific implementation, because it assumed the data fetch was fresh when it was not.

What to take from this

  • Rendering freshness and data freshness are different caches. Fixing one does not fix the other.
  • Check defaults before blaming them. An unset option is not a misconfigured option.
  • Probe both sides of a cache boundary. One request before, one after. It converts an argument into a fact in about thirty seconds.
  • Be suspicious of fixes that make unknown input more expensive to serve. That is usually a vulnerability wearing a bugfix costume.

The embarrassing version of this bug is that everything was working correctly. The router did its job. The cache did its job. They just disagreed about what time it was.