
Avoid this frustrating Next.js App Router quirk where `Suspense` forces streaming mode and breaks `notFound()` 404 status codes.
While working with Next.js App Router (v13+), I discovered a frustrating issue: notFound() doesn't return a proper 404 HTTP status code when Suspense is anywhere in your rendering path.
Here's what happens:
Suspense is present, Next.js automatically enables streaming modeHTTP 200 OK initially404 status never reaches the clientThis isn’t just a theoretical issue. It has real consequences:
200 OK instead of 404, potentially indexing non-existent pagesThe worst part? Next.js basically forces you into this trap. Want to use useSearchParams? Cool, wrap it in Suspense. Congrats, you just nuked your 404 status codes:
// app/search/page.tsx
'use client'
import { useSearchParams } from 'next/navigation'
export default function SearchPage() {
const searchParams = useSearchParams()
const query = searchParams.get('q')
return <div>Searching for: {query}</div>
}This will fail at build time with:
Error: useSearchParams() should be wrapped in a suspense boundary at page "/search".
Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailoutUnfortunately, were’re forced to wrap it in Suspense at a page level (page.tsx):
// app/search/page.tsx
import { Suspense } from 'react'
import SearchComponent from './SearchComponent'
export default function SearchPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchComponent />
</Suspense>
)
}But now you've enabled streaming mode, and any notFound() call in your page will return 200 OK.
Here’s a quick live demo on one of my side projects. It will return a fully rendered not-found.tsx with a HTTP status code 200 instead of 404, because I had to wrap the main layout in Suspense component, due to having global search in the header.
There are a few solutions to this issue I’ve tried successfully, though in the end I had to go back to streaming due to convenience.
Suspense on 404-critical pagesIf possible, structure your pages so critical 404 checks happen outside Suspense boundaries. Doesn’t work if you need useSearchParams or similar hooks at the page level.
Instead of useSearchParams, fetch the data through a Route Handler (API route). It’s more complex, requires additional API routes, client-side fetching.
Handle 404s at the middleware level before rendering. This adds latency, duplicates validation logic, and is not suitable for all cases (actually, it should be avoided).
Only use Suspense when absolutely necessary. This adds complex logic and it still doesn’t solve the fundamental issue.
Sometimes the best solution is to make peace with the trade-off:
// app/products/[id]/page.tsx
/**
* NOTE: This page uses Suspense for client components,
* which means notFound() will return 200 OK due to streaming.
*
* For SEO purposes, we rely on:
* - proper canonical URLs
* - Next.js automatically injects noindex meta tag
* - sitemap exclusions
*/
export default async function ProductPage({ params }: { params: { id: string } }) {
// ...
}Note: Next.js automatically injects <meta name="robots" content="noindex" /> when notFound() is called, preventing search engines from indexing these pages.
This issue stems from how HTTP streaming works:
200 OKnotFound()Next.js can’t retroactively change the status code once streaming begins.
I think Next.js could handle this way better than it does now. There are a few potential solutions I could think of:
export const dynamic = 'no-stream'notFound() usage within Suspense boundaries)This is a fundamental tension in Next.js App Router between:
Until Next.js can provide a better solution, there’s not much we can really do as developers. We can document and monitor, implement above workarounds, or try to avoid this altogether.
This issue is why I’m not a fan of streaming and would prefer the “interactive islands architecture” pattern from frameworks such as Astro.
It makes things not only more difficult to debug, but also tricky to deal with. A HTTP status codes are a critical signal to external services interacting with our content and should not be taken lightly.
References: