You open Google Search Console, scroll to the Pages report, and there it is: a growing pile of URLs labeled "Soft 404." Nothing is technically broken. Your server returns 200 OK. The page renders. And yet Google has decided the page is effectively missing.
Soft 404s are sneaky because they don't trip any alarm a developer normally watches. Your uptime monitor is happy. Your error logs are clean. Meanwhile, crawl budget leaks away, thin pages dilute your site's quality signals, and your SEO fix list keeps growing with items nobody can explain.
Let's fix that.
What a soft 404 actually is
A soft 404 happens when a URL says it's fine (HTTP 200) but looks like it isn't. Google compares the status code with the content and notices the mismatch. Common causes:
- A "Product not found" or "No results" template served with a 200 status.
- Deleted pages that redirect everything to the homepage.
- Empty category, tag, or search pages with no real content.
- JavaScript apps that render an error state client-side while the server always returns 200.
- Placeholder pages ("Coming soon!") with a few words and nothing else.
The core issue is honesty. If a page doesn't exist, the server should say so. If it moved, the server should say where.
Why they hurt more than real 404s
A real 404 is a clear signal: "nothing here, stop asking." Google drops it and moves on. A soft 404 is ambiguous, so crawlers keep returning, spending crawl budget on junk instead of your new content.
Worse, soft 404s hide real problems. A product that was quietly removed, a broken database query that returns zero rows, a misconfigured route — all of these can masquerade as "pages that work." When you finally investigate, you're debugging months of silent drift.
Spotting soft 404s with curl
You don't need a fancy crawler to start. curl tells you exactly what the server claims.
1. Check the status code of a URL that shouldn't exist:
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/this-page-does-not-exist-12345
If that prints 200, your site is likely producing soft 404s for every missing URL. It should print 404 (or 410).
2. See status, final URL, and size together:
curl -sL -o /dev/null \
-w "status=%{http_code} final=%{url_effective} bytes=%{size_download}\n" \
https://example.com/old-product
A tiny bytes value with a 200, or a final URL that's just your homepage, is a red flag.
3. Look for redirect chains to the homepage:
curl -sIL https://example.com/discontinued-item | grep -iE "^(HTTP|location)"
If an old deep URL ends at /, Google will often treat it as a soft 404. Redirect to the closest relevant page, or let it 404/410.
4. Grep the body for "not found" language served as 200:
url="https://example.com/search?q=zzzzzz"
code=$(curl -s -o /tmp/body.html -w "%{http_code}" "$url")
if [ "$code" = "200" ] && grep -qiE "not found|no results|doesn.t exist|0 items" /tmp/body.html; then
echo "Possible soft 404: $url"
fi
5. Batch-check a list of URLs:
while read -r u; do
printf "%s\t%s\n" "$(curl -s -o /dev/null -w '%{http_code}\t%{size_download}' "$u")" "$u"
done < urls.txt | sort -n -k2
Sorting by size surfaces the thinnest 200-status pages first — usually where soft 404s live.
How to fix them (by cause)
Missing content served as 200. Change the handler to return a real status. In Express:
app.get('/products/:slug', async (req, res) => {
const product = await findProduct(req.params.slug);
if (!product) return res.status(404).render('not-found');
res.render('product', { product });
});
Keep the friendly not-found template — just send it with the right code.
Single-page apps. If your server always returns 200 and the router handles errors client-side, either pre-render with server-side status codes, or add <meta name="robots" content="noindex"> dynamically on error states as a fallback. Real status codes are better.
Mass redirects to the homepage. Map old URLs to their nearest equivalent (a parent category, a replacement product). If there's no equivalent, use 410 Gone.
Thin or empty pages. Either add genuinely useful content, consolidate them, or noindex them. Empty tag archives and zero-result search pages usually deserve noindex or a 404.
Short links and campaign URLs. Expired or mistyped short links often fall through to a generic 200 landing page. Make sure your shortener returns a proper 404 for unknown slugs. If you're picking one, check that behavior first — a tool like this URL shortener is worth testing with the curl commands above before you trust it with campaign traffic.
The practical soft 404 checklist
Copy this into your next sprint:
- [ ] Request a random nonsense URL on every major route pattern; confirm
404/410. - [ ] Export Search Console's "Soft 404" report and group URLs by template.
- [ ] Run the batch curl script; flag 200-status pages under ~2 KB.
- [ ] Grep 200 responses for "not found," "no results," and "out of stock" templates.
- [ ] Audit redirects: no deleted deep pages should land on the homepage.
- [ ] Return 404 from the server for missing records (not just in the UI).
- [ ]
noindexor remove empty tag, category, and internal search pages. - [ ] Confirm short-link and campaign domains 404 on unknown slugs.
- [ ] Remove soft-404 URLs from your XML sitemap.
- [ ] Use "Validate Fix" in Search Console after deploying.
- [ ] Add a CI check that asserts a fake URL returns 404.
That last item is the one that keeps the problem from coming back:
code=$(curl -s -o /dev/null -w "%{http_code}" "$STAGING_URL/__definitely-missing__")
[ "$code" = "404" ] || { echo "Soft 404 regression: got $code"; exit 1; }
Wrapping up
Soft 404s aren't dramatic. They don't crash anything. That's exactly why they pile up. The fix is mostly about honesty between your server and the crawler: return the status code that matches reality, redirect only when there's a real destination, and stop indexing pages with nothing on them.
Spend an afternoon with curl and the checklist above, and a surprising chunk of your SEO fix list will simply disappear. For more practical write-ups like this, I keep notes on the blog.
Written by Riichi Mirza
Top comments (0)