A soft 404 is the one indexing state where Google disagrees with your server and wins. You returned 200 OK. Google looked at the response, decided it was functionally an error page, and dropped it from the index anyway.
That makes it different from every other indexing problem. noindex, canonicals, and robots rules are instructions you issued. A soft 404 is a classification applied to your content, which means you can't fix it by editing a directive. You have to change either what the page returns or what it says.
What Google actually means by "soft 404"
Google's documentation defines a soft 404 as a page that returns a success status code but whose content tells the user the page doesn't exist — and it recommends returning a real 404 or 410 for pages that are genuinely gone. Search Console surfaces these under the Page indexing report as their own reason, separate from "Not found (404)".
What Google has not documented is the classifier. There is no published list of trigger strings or a word-count threshold. What we can say from observed behavior across many sites is that the signals cluster into a few reliable groups: explicit error language in the rendered text, an effectively empty main content area, and boilerplate-only pages where the template renders but the unique content slot is blank.
Treat the categories below as strong inference from repeated observation, not as documented rules. The remediation advice is the documented part.
Why a soft 404 is worse than an honest 404
A real 404 is cheap. Google records the status, indexes nothing, and progressively backs off re-crawling that URL. The signal is unambiguous and resolves in one request.
A soft 404 costs more in three ways:
- It consumes fetch and render budget repeatedly. Google keeps treating the URL as a live page worth re-checking, because the server keeps insisting it's fine. On JavaScript sites, that means the renderer runs too.
- It hides real failures. If the same template produces a soft 404 for one URL because content is missing, and for another because a client-side API call timed out, Search Console shows you one bucket and you can't tell them apart without your own instrumentation.
- It can scale to whole sections. Soft 404 classification is per-URL, but the cause is almost always per-template. One broken empty-state path across a faceted catalogue produces thousands of them at once.
The four patterns that produce most soft 404s
1. Error copy served with a 200. The classic: a custom error page that the CMS renders through the normal template at whatever URL was requested. "Sorry, we couldn't find that page", "No results for…", "This product is no longer available". The text says error; the header says success.
2. Client-side data fetching that fails for Googlebot. The HTML shell arrives with 200, the JavaScript runs, the XHR to your content API returns a 403 or times out, and the rendered DOM has a header, a footer, and nothing in between. Googlebot sees a page with no content. This is the most common flavor on SPA and app-router sites, and the hardest to reproduce, because it often only fails under rate limiting or geo/IP rules that hit crawlers and not your browser.
3. Legitimate empty states. Zero-result internal search pages, filter combinations with no matching products, category pages after inventory churn, expired events and job listings. These are correct application behavior and a soft 404 by content.
4. Redirecting removed pages somewhere irrelevant. Google's documentation explicitly notes that redirecting a deleted page to an unrelated page — the homepage being the usual offender — can be treated as a soft 404 rather than a redirect. A 301 does not launder a removal.
How to detect them at scale rather than one at a time
Search Console is the starting point but not the whole picture. The Page indexing report caps the example URLs it shows per issue, so on a large site you're looking at a sample, not an inventory. Use the URL Inspection API to confirm indexing state for specific URLs you care about, and build your own detector for coverage.
The detector is simple: crawl with rendering enabled and assert on the rendered output.
soft_404_suspect =
status == 200
AND (
main_content_words < 50
OR matches_error_phrase(rendered_text)
OR rendered_text == boilerplate_only
)
AND NOT has_noindex
A few implementation notes that matter more than the pseudocode:
- Measure words in the main content region, not the whole document. Nav and footer boilerplate will float a genuinely empty page above any naive threshold.
- Compare the raw HTML against the rendered HTML. A page that's empty in both is a content problem. A page that's populated server-side but empty after render is a hydration or fetch problem.
- Run the crawl from a datacenter IP with a Googlebot user agent and, separately, with your own. Divergence between the two is exactly the failure mode in pattern 2.
- Cross-reference your logs. URLs fetched repeatedly over weeks with no impressions are candidates whether or not Search Console has flagged them yet.
Which status code each empty state deserves
| Scenario | Return | Why |
|---|---|---|
| Product or page permanently removed | 404 or 410 | Unambiguous removal; 410 is the stronger assertion of the two |
| Out of stock, returning later | 200 with real content | The page still has purpose; reflect availability in your Product markup |
| Removed, close equivalent exists | 301 to the equivalent | Only if genuinely equivalent — not to the homepage |
| Zero-result internal search | 200 + noindex, ideally not crawlable | It's a valid UX state and a worthless index entry |
| Filter combination with no matches | 404, or noindex if transient | Depends on whether the combination can ever have results |
| Expired event or job | 404/410, or 301 to the parent listing | Keep the archive only if the archive is useful to a searcher |
| Backend or API failure | 503 | Transient by definition; 200 with an empty shell is the bug |
The last row is the one engineering teams get wrong most often. A failed data fetch must not produce a 200. In Next.js, notFound: true gives you a real 404 and a thrown error gives you a 500 — both are better than rendering a skeleton successfully. If the dependency is down rather than the record missing, prefer 503 with a Retry-After so Google backs off instead of recording emptiness.
What to do this week
- Export every URL in the Search Console soft 404 bucket and group them by URL pattern. You are looking for templates, not pages.
- For the top two or three patterns, fetch one URL with rendering as Googlebot from outside your network and compare against your browser. Different output means an access or rate-limit problem, not a content problem.
- Audit your error and empty-state handlers for status codes. Any handler that can emit
200with error copy is a defect; fix the header, not the copy. - Find every
301that points at the homepage and decide, per group, whether the correct answer is410or a relevant target. - Add the rendered-main-content assertion to whatever crawl you already run on a schedule, with an alert on count rather than on individual URLs. Soft 404s arrive in batches when a template changes, and the count is what tells you.