With the database stopped, our homepage still answered 200.
Not an error page. Not a maintenance notice. A normal-looking response with the header, the navigation and the footer intact, and nothing at all where the site goes — none of its sixteen article links, a fifth of its usual text. Any monitor watching for a success code saw a healthy site for the entire outage.
That page is not only an outage symptom. It is a shape a page can take at any time, for reasons that never produce an error: a query returning nothing after a bad deploy, a template failing silently, a feature flag off in the wrong environment. Finding one is the problem. Our first attempt failed, and this is an honest account of that before what worked.
First, measure the right thing
The obvious measure is response size, and it understates the problem badly:
normal hollow share
bytes 160,490 85,587 53.3%
visible text 9,323 1,935 20.8%
By bytes the page looks half-normal, which is within shouting distance of an unremarkable page. By the text a reader could actually see, it is a fifth. The difference is markup and chrome — scripts, styles, navigation, structured data — which are close to a constant and swamp the content you are trying to weigh.
Strip the tags and count what is left. It is the same one-line transformation either way, but only one of them produces a signal you can threshold.
The detector that did not work
The natural idea: group pages by type, take the median for each type, and flag anything well below it. We sampled 120 live pages — 30 each of guides, news articles, tool pages and directory entries — and flagged everything under 60% of its type's median.
guide n=30 median 12,440 chars
news n=30 median 13,697 chars
tool n=30 median 11,264 chars
ai_directory n=30 median 8,459 chars
flagged: 15
genuine problems: 0
Fifteen flags, none of them real. They were category hubs, the guides index, a thread listing, and ?lang= variants of tool pages — all of them legitimately shorter pages that happened to sit in a sitemap alongside full articles.
The flaw was not the threshold, but the assumption that a page "type" is homogeneous. A median only means something across pages that are trying to be the same shape, and a sitemap groups pages by subject, not by shape. Lower the threshold to cut the false positives and you also stop detecting anything.
The detector that did
Compare each page against itself instead. The question becomes "is this page smaller than it was", not "is it smaller than its peers".
That only works if healthy pages are stable, so we measured that rather than assuming it — six URLs, four fetches each, spread over minutes:
0.47% homepage
0.45% a guide
0.41% another guide
0.00% the news index
0.00% the directory index
0.00% a tool page
worst variation between fetches: 0.47%
the hollow page, against its own normal: 20.8%
Two orders of magnitude apart. A healthy page barely moves, so almost any threshold between them separates the two cleanly — and unlike the type median, the comparison never asks two different pages to look alike.
The catch, and it is the whole point
You need the baseline before the incident. A per-URL history is worthless if you start recording it during the outage, because the first measurement you take of a broken page becomes its normal.
This is why the technique tends to be skipped: it costs nothing and it pays nothing, right up until the day it pays everything. There is no way to build it retroactively. Fetch the pages you care about on a schedule, store one integer each, and let it accumulate boringly.
for each url you care about:
text = strip_tags(fetch(url))
record(url, len(text), now)
if median(history(url)) and len(text) < 0.6 * median(history(url)):
alert(url, len(text), median(history(url)))
One number per page per run. A hundred pages checked hourly is a few thousand integers a day, which is nothing, and it is the only thing that would have caught our homepage while it was cheerfully returning 200.
What this does not catch
A page that is the right size and the wrong content. Stale data, another tenant's records, last month's prices — all of those weigh the same as the truth. Size is a crude proxy. It catches the kind of unsubtle failure that currently goes unnoticed for hours.
It also will not catch a page that degrades gradually. If a section quietly stops rendering and takes 8% of the text with it, that sits inside the range you would have to allow for ordinary editorial change, and no threshold on a single number will separate it.
How this was measured
120 live pages sampled evenly from four sitemaps, fetched read-only with a self-identifying user agent and a quarter-second pause between requests. Visible text is the page with scripts, styles and tags removed and whitespace collapsed. The stability test is six URLs at four fetches each. Every figure is a count from those runs.
The sweep found nothing broken. So detector A's precision was measured against a healthy corpus — it produced fifteen false positives and had no true positives available to find. That result speaks to its usefulness on this site; it is not a claim that the method can never work.
The hollow page was made deliberately. We stopped a database on a development stack rather than catching a production incident. The honest claim is narrower than the exciting one: this detects a page whose content has collapsed, but only if you were already recording its baseline.
Six URLs is enough for one point. It shows healthy pages are stable to well under one percent. It does not characterise every page type on the site, and a page carrying live counts or a rotating feed will be noisier than anything measured here.