A page that returns 200 when you open it in your browser can still be failing for search engines. We hit this on a Next.js site: the service pages loaded fine on every manual check, yet Bing reported them as 404 and Google slowly dropped them from the index. The cause was an intermittent failure in the data layer. Roughly three requests out of four came back as 404 with a noindex tag, and the fourth one, the one a human happened to see, was fine.
One request per URL will never catch that. You need to ask the same question many times and look at the distribution of answers.
The check: request every URL N times
The script below is plain Python 3 with no dependencies. It reads URLs from a file, requests each one N times with a short pause, and prints how often each status code appeared. It also flags a noindex in the response, because an error page that carries noindex is what actually gets a page dropped.
#!/usr/bin/env python3
"""url_stability.py - request each URL N times and report status-code distribution.
usage: python3 url_stability.py urls.txt [N]"""
import sys, time, collections, urllib.request, urllib.error
UA = "Mozilla/5.0 (stability-check)"
def fetch(url):
req = urllib.request.Request(url, headers={"User-Agent": UA, "Cache-Control": "no-cache"})
try:
with urllib.request.urlopen(req, timeout=20) as r:
body = r.read(200_000).decode("utf-8", "ignore").lower()
return r.status, "noindex" in body
except urllib.error.HTTPError as e:
body = e.read(200_000).decode("utf-8", "ignore").lower()
return e.code, "noindex" in body
except Exception as e:
return type(e).__name__, False
def main():
urls = [u.strip() for u in open(sys.argv[1]) if u.strip() and not u.startswith("#")]
n = int(sys.argv[2]) if len(sys.argv) > 2 else 20
unstable = 0
for url in urls:
codes, noindex = collections.Counter(), 0
for i in range(n):
code, ni = fetch(f"{url}{'&' if '?' in url else '?'}_={time.time_ns()}")
codes[code] += 1
noindex += ni
time.sleep(0.5)
ok = codes.get(200, 0)
flag = "OK " if ok == n else "BAD"
unstable += ok != n
print(f"{flag} {ok}/{n} ok {dict(codes)} noindex={noindex} {url}")
sys.exit(1 if unstable else 0)
if __name__ == "__main__":
main()
A run against a healthy site prints one OK 20/20 line per URL. On the site we were debugging it printed lines like this:
BAD 5/20 ok {404: 15, 200: 5} noindex=15 https://example.com/service/seo
That single line explained weeks of confusing reports.
Details that matter
-
Bust caches on purpose. The
_=query parameter forces a fresh response from CDNs and ISR caches. Run the check a second time without it to see what cached visitors and crawlers get. The two results can differ, and the difference tells you which layer is broken. - Twenty requests is a floor. Twenty requests per URL is enough to see a 25% failure rate reliably. For rarer failures, run it on a schedule and keep the output.
-
Exit codes make it CI-friendly. The script exits
1if any URL is unstable, so you can run it after every deploy and fail the pipeline. -
Check the bots too. Change the
User-Agentto a crawler string to confirm that bots are not treated differently. If only one user agent fails, look at the middleware, the WAF and the bot rules first.
Typical causes once you find it
-
A CMS or API call fails and the page calls
notFound(). In Next.js App Router code,if (!data) notFound()turns every upstream timeout into a real404withnoindex. Fail loudly instead: return a5xx(search engines retry those) or serve the last good copy. - Two app versions behind a load balancer. One instance knows the new routes and the other does not, so the response depends on which instance answers. Compare response headers, such as the build ID and server timing, between good and bad responses.
-
Edge caching an error. A single bad response cached for an hour looks like an outage. Make sure error responses are sent with
Cache-Control: no-store.
After the fix
Re-run the check with N=50 and keep the log. Then resubmit the affected URLs in Search Console and Bing Webmaster Tools, and watch the coverage reports over the following week. For redesigns, add a permanent 301 from every old path to its new one. A missing redirect looks exactly like this problem from the crawler's side.
If you want a second pair of eyes on a platform or migration like this, our team covers it under web platform development.
References
- Google Search Central: HTTP status codes and how they affect Google Search
- Next.js docs:
notFound - Bing Webmaster Guidelines: Bing Webmaster Tools help
Top comments (0)