A soft 404 is not something your server returns. It is something Google decides about a page that returned 200 OK. That distinction matters, because it means no amount of checking status codes in your crawler will find the problem. Your crawl report will show a clean wall of 200s while Search Console quietly drops a few thousand URLs into a bucket labelled "Soft 404".
Most teams discover this after the fact, when the Page Indexing report jumps and nobody changed anything. The underlying cause is usually a template that handles a missing, empty, or unavailable resource by rendering a friendly message instead of signalling failure at the HTTP layer.
What Google actually means by "soft 404"
Google's documentation defines a soft 404 as a page that returns a user-friendly "not found" message without a corresponding 404 or 410 HTTP status code. Search Console surfaces it as a distinct state in the Page Indexing report, and the URL Inspection API returns it in coverageState with a string like Submitted URL seems to be a soft 404.
The mechanism is a content classifier, not a rule. Google is looking at the page it fetched and rendered and deciding whether it looks like an error page. Google has not published the feature set, so treat specifics as inference rather than fact. What is well-established from Google's own guidance and public statements:
- Pages whose visible text says some version of "not found", "no longer available", "page doesn't exist", or "no results" are candidates, in any language.
- Pages with essentially no primary content — an empty category, a search results page with zero hits — are candidates even without explicit error wording.
- Google has said that product pages for permanently unavailable items can be treated as soft 404s, which is why "out of stock" templates that strip the description and price are risky.
The reasonable inference, given how the signal behaves across sites, is that the classifier combines rendered text patterns with how little unique content remains relative to the rest of the template. It is not a keyword blocklist; a page that mentions a 404 error in a blog post about HTTP status codes does not get flagged.
Why returning 200 for missing content is the actual bug
The status code is the only unambiguous channel you have for telling a crawler that a URL has no resource behind it. When you overload a 200 with an error message, you force Google to guess, and you pay for the guess twice.
First, discovery. Google documents that it crawls and recrawls based on its model of a URL's value. A URL that returns 200 enters the normal recrawl rotation. A URL that returns 404 or 410 exits it quickly — Google has documented that 410 is treated slightly faster than 404, but both are terminal signals. Soft 404 pages keep getting crawled until the classifier is confident, then get crawled at a reduced rate afterwards.
Second, index coverage reporting. Soft 404s are not indexed, so they look like an indexing problem in Search Console. If the pages genuinely should not exist, the right fix is a status code change, not an indexing investigation. Teams routinely burn weeks on the wrong diagnosis here.
The patterns that trip the classifier
| Page pattern | Why it looks like an error | Correct handling |
|---|---|---|
| Internal search with zero results | No primary content, template-only text | noindex, or block the search path in robots.txt |
| Empty category or filter combination | Same as above, often infinite in count | Return 404 for impossible combinations; noindex thin-but-valid ones |
| Deleted product served as "no longer available" | Explicit error wording, stripped content | 404/410 if gone; redirect to a parent if a real equivalent exists |
| Out-of-stock item with content removed | Little left but boilerplate | Keep the full description and reviews; mark availability in schema |
| JS app rendering an error state client-side | Server says 200 before the app runs | See the next section |
| Geo or auth gate that blanks the page for Googlebot | Rendered page has no content | Serve the public content; do not gate on user agent |
JavaScript error pages are the hardest case
A single-page app cannot set a status code from the client. The server returns 200 and the shell, the router resolves a route that does not exist, and the component renders "Page not found". Googlebot renders it, sees error text, and classifies accordingly — if you are lucky. If rendering is deferred or fails, it may index the empty shell instead.
Google's JavaScript SEO documentation gives two sanctioned workarounds, and both are worth implementing together:
- Redirect to a URL that the server answers with a real 404 or 410. This is the cleaner option because the signal reaches Google at the HTTP layer.
- Inject
<meta name="robots" content="noindex">into the head when the error state renders. This keeps the URL out of the index, but it does not tell Google the resource is gone, so recrawling continues.
// Option 2, in the error route component
const meta = document.createElement('meta');
meta.name = 'robots';
meta.content = 'noindex';
document.head.appendChild(meta);
If your framework supports server-side rendering, the better answer is to resolve the route on the server and return the correct status with the error HTML in one response.
How to audit for soft 404s yourself
Search Console tells you after Google has already decided. You can get ahead of it with a crawl you already run.
- Measure body text length, not response size. Extract visible text from the rendered DOM and build a distribution per template. Soft 404 candidates cluster in a thin left tail — a product template whose median is 1,800 words of text with a cluster at 60 is showing you the deleted-product state.
- Grep the rendered DOM for error strings in every language you publish: "not found", "no results", "sold out", "no longer available", plus your own template's wording. Then check the status code those URLs returned. Any match with a 200 is a candidate.
- Compare raw HTML to rendered HTML. If the raw response contains a full page and the rendered one contains an error, your client-side router is the culprit. If the reverse, your SSR layer is masking a failure.
- Sample with the URL Inspection API. Pull
coverageStatefor a stratified sample of each template and count the soft 404 verdicts. This is the only way to see Google's actual classification rather than your proxy for it. - Check your logs for 200s with tiny response bodies on URLs Googlebot requests repeatedly. Repeated crawling of near-empty responses is the crawl cost you are paying while the classifier makes up its mind.
What to do about it
- Decide, per template, whether the resource exists. If it does not and never will, return 410. If it is temporarily gone, return 404 and keep the URL in your sitemap only if you expect it back.
- Stop stripping content from pages that still have a reason to exist. An out-of-stock product with its description, specs, reviews, and a clear availability state is a real page; a page that says "out of stock" and nothing else is not.
- Make every faceted or search URL that can return zero results either
noindexor a hard 404, and make the rule deterministic so you can test it. - Add an assertion to your release tests: fetch a known-missing URL per template and fail the build if it returns 200 with error text.
- Re-sample
coverageStatethrough the URL Inspection API a few weeks after shipping the fix. Soft 404 counts in the Page Indexing report move slowly because recrawl rates for these URLs are already suppressed; the API gives you per-URL confirmation faster than the aggregate chart does.