Next.js notFound() Returns 200: Test Status and Noindex
A measured Next.js 15.1.4 production fixture compares early and streamed notFound responses, with downloadable source and results.
A not-found screen and an HTTP 404 are related, but streaming can separate them. If the response has already started, Next.js can show not-found handling while the network response remains 200. Inspect the response, robots directives and page behavior separately.
The Next.js not-found documentation distinguishes streamed and non-streamed responses. The notFound function reference describes the noindex behavior. Your framework version and route structure still need a test.
What we actually tested
On September 28, 2026, we built an isolated local app with Next.js 15.1.4 and React 19.0.0, then ran its production server. It reused the installed dependencies for those exact versions. This is a production-build fixture, not a test of a live customer's site, a CDN or Google's indexing system. It is not evidence for every newer Next.js release.
We made GET requests using an ordinary curl user-agent string and a simulated Googlebot string. Both produced these status/directive results:
| Route | Fixture behavior | Observed status | Noindex in response markup |
|---|---|---|---|
/known | Normal dynamic page | 200 | No |
/early | Calls notFound immediately | 404 | Yes |
/late | Loading boundary; waits 250 ms, then notFound | 200 | Yes |
/does-not-exist | No matching route | 404 | Yes |
Download measured results and the complete fixture source. The raw response may include serialized not-found text even for a successful route, so searching for the phrase “not found” alone is not a reliable test. The results distinguish literal heading markup from arbitrary response text.
The delayed fixture
The essential pieces were:
// app/late/loading.jsx
export default function Loading() {
return <p>Loading fixture resource</p>
}
// app/late/page.jsx
import { notFound } from 'next/navigation'
export const dynamic = 'force-dynamic'
export default async function Page() {
await new Promise(resolve => setTimeout(resolve, 250))
notFound()
}
The delay is deliberately artificial and only exists to demonstrate a response starting before the missing-resource decision. It is not a recommended application architecture.
Diagnose your route
- Pin the version and reproduce the affected request with a production build.
- Include one known resource and one missing resource. Otherwise a global error can look like correct missing-page behavior.
- Save the status, redirect chain, full HTML and robots directives for each request.
- Record any loading/Suspense boundary before the lookup that calls notFound.
- Inspect the rendered page as well as the original response. Test the deployed host separately because middleware, rewrites and CDN errors can change the result.
Use the raw-versus-rendered HTML diff with two captured snapshots. It compares supplied text; it does not run a renderer or establish Google's indexed view. The noindex tool can interpret a copied directive.
Choose a fix based on the intended behavior
| Situation | Investigation |
|---|---|
| Missing item detected before the response starts | Keep the lookup and missing-resource decision before streaming where practical |
| Known item unexpectedly gets noindex | Trace error handling and inherited metadata; do not remove all safeguards globally |
| Missing item returns a normal 200 shell without appropriate handling | Call the framework's missing-resource path and retest |
| Local fixture differs from deployment | Compare middleware, route rewrites, host responses and exact framework version |
Moving work before a loading boundary can change perceived performance. Measure the tradeoff instead of removing streaming everywhere to satisfy an audit label. Never set all unknown URLs to redirect to the homepage as a substitute for deciding whether a real replacement exists.
What the test cannot prove
Changing a request's user-agent string does not make it an authentic Googlebot request. A local 200 with noindex is not proof that Search Console will call the page a soft 404. After deployment, use URL Inspection and record Google's actual observed crawl/response separately.
For the general diagnosis, read soft 404 errors. For tags delivered after initial rendering, see App Router metadata. Keep the fixture result with the version so upgrades trigger a new test rather than an assumption.