Nextjs ISR on-demand revalidation with tags and dynamicParams in app router causes all routes to 404

Hello Support,

I’d like to build a Nextjs site that is statically rendered and served until on demand revalidation of nextjs cache by tags. I’d like to have the dynamicParams page variable set to false so that any routes that aren’t known instantly 404.

I set up a POC at https://stanford-emergency-poc.netlify.app/
Repo: GitHub - SU-SWS/emergency-scale-poc: For POC and load testing cached responses and dynamic polling

The initial build works great and I get a rendered and cached page. Refreshing the page keeps the dynamic content static. Navigating to pages not in the known slugs (/,/resources) causes a 404.

However, when I invalidate the nextjs cache using tags or path, ALL the routes 404 and nothing gets refreshed.

If I remove the page.tsx setting of export const dynamicParams = false; everything works fine. However, I want to protect my downstream API from being pinged for every route and would like to prevent unknown routes from hitting my downstream API.

Is this a known bug or limitation?

Thanks

Just to follow up on the POC

Included is an invalidate cache through a server action and through an API route. Both have the same behavior.

If you get a 404 you can click on the rebuild site button and wait a minute to rebuild the nextjs cache and have the site working again.

The error in the function log isn’t informative as it looks like

I believe the error is a red-herring. We had someone ask about 404s throwing errors and the answers given by devs was:

The log line is produced here: https://github.com/netlify/serverless-functions-api/blob/824fc1ecdcbc5dd3465f135c238bae56cbbab6d0/src/context.ts#L122

And the waitUntil call stack is:

    at Object.waitUntil (file:///var/task/___netlify-bootstrap.mjs:2:21727)
    at Object.trackBackgroundWork (/var/task/.netlify/dist/run/handlers/request-context.cjs:137:17)
    at trackedFn (/var/task/.netlify/dist/run/next.cjs:510:28)
    at /var/task/node_modules/.pnpm/next@15.2.4_@babel+core@7.26.9_@opentelemetry+api@1.9.0_react-dom@19.0.0_react@19.0.0__react@19.0.0_sass@1.86.0/node_modules/next/dist/lib/batcher.js:45:38
    at process.processTicksAndRejections (node:internal/process/task_queues:77:11)

which is for next-runtime internal tracking of Next’s background work - the tracking itself does work, but Next or customer seems to throw/reject the work with “Nothing” error (?) as this is what is being logged to stderr:

ERROR  [Error: ]

I have some ideas here to check - this might be result of some briliant Next.js / React ideas to use throw as “clever” execution control and not indicate actual failure anywhere, but given low prio it will probably have to wait a bit until more urgent escalations are handled

Once waitUntil was rolled out, next-runtime automatically started using it because it was ready for it.

Previous handling in next-runtime was not logging rejected promises, while waitUntil does so this is pretty good explanation why it suddenly started to show in logs. Maybe we should add silencing of a promise to next runtime to restore previous behavior (and potentially just debug log it, so it doesn’t confuse users) - likely things were throwing/rejecting before - it’s just now it’s being actually logged

I’ll check the rest of the issue shortly…

1 Like

As for the original issue, I don’t see anything obviously wrong, so I have asked the devs to take a look.

1 Like

Thank you very much.

They devs mentioned:

There look to be a mix of a couple different issues here.

The handling of dynamicParams = false appears to be the default handling coming from Next.js. When running the site locally, outside the scope of Netlify, I am able to reproduce the behaviour of invalidated pages returning 404s. The way to reproduce this locally:

  • npm run build + npm run start
  • curl -X "POST" http://localhost:3000/api/invalidate-cache

While this may not the expected behaviour, we don’t have much influence over the fix. The best way forward would be to open an issue with Next.js.

As a potential workaround, they could use dynamicParams = true and return notFound() when the route is unknown prior to fetching from their API (this assumes that they have a concrete list of all the routes upfront similar to their reproduction repository).

Another potential way forward (depending on their use case) is to override the default revalidate = false with a concrete time (e.g. 60 seconds). This doesn’t give them the same fine-grained cache control as on-demand revalidation, however it does still prevent their downstream API from receiving no more than one request every X seconds (where X is the value they configure).

The second issue is surrounding all of their routes becoming invalidated when they call their invalidation endpoint, which has to do with how they’ve configured their fetch route. From their code, they are placing a tag on the fetch call in the component. See here:

const resp = await fetch('...', { next: { revalidate: 316857, tags: [ 'cards'] } });

The way that next handles this is by placing the cards cache tag on the page as well, such that when they then call revalidateTag("cards") all pages with that cache tag are revalidated. Seeing as all the routes use the same fetch call with the same cache tag, the result is all of their pages are getting invalidated. I don’t believe there is anything for us to fix here, this seems to be just how things work.

The following was something I noticed as well, but I didn’t want to bring that up till we solved the issue, but the devs have also mentioned it, so I’ll quote it below as well:

Finally, a helpful tip. They are calling purgeCache directly in their code, which isn’t necessary or advised. The next-runtime will make the appropriate purge calls for them based on their revalidatePath and revalidateTag calls.

1 Like

Thank you very much for the investigation and the extra tips @hrishikesh. I’ve already started looking into the solution of handling the 404 downstream API protection.

One question I have is, do these 404 hits to the ‘server’ count as function invocations, and do they count towards the total allotted concurrency of function executions?

eg: We have a maximum concurrency of 1000 functions across our account and if some bad actor decided to run a DoS against the site, or some vulnerability scanner ran too aggressively, would there theoretically be a chance that it could impact our other web properties as each 404’d route would run a function and could potentially exhaust our allocation?

Thanks

Yes, they do count towards the concurrency.

1 Like

Thank you for confirming.