The Page Loads, But It's Broken — Catching Failures That Return 200
There's a category of outage your uptime monitor will cheerfully sleep through: the site is "up," the server answers in 200 milliseconds with a proud 200 OK — and the page is blank. Or the product grid is empty. Or checkout dies on step two. Or your API returns {"error": "database unavailable"} with, yes, status 200. These are soft failures, and they're arguably worse than hard downtime, because hard downtime gets detected in one check cycle while soft failures run for hours with your dashboard glowing green.
Why status-code monitoring misses an entire class of failure
A status code tells you one thing: the web server produced a response. It says nothing about whether that response contains a working page. The gap between those two is where soft failures live, and they're depressingly common:
- The error page that returns 200. Plenty of frameworks and CMSes render their error state — "Fatal error," a stack trace, a white screen with a PHP warning — as a normal 200 response. The server did its job; the application inside it did not.
- The empty shell. JavaScript-heavy sites often serve a valid HTML skeleton and let the client render everything. If the JS bundle 404s, or an API call inside it fails, the server keeps returning a flawless 200 for a page that displays precisely nothing.
- The dead dependency. The page renders, but the piece that matters is gone — the product grid is empty because the search index died, prices are missing because a pricing service timed out and the template swallowed the error.
- The API that reports errors politely. Some APIs — more than anyone will admit — return errors in the body with a 200 status. Your monitor sees 200 and files it under "healthy."
If your monitoring only asserts on status codes, this whole class of incident is invisible until a customer emails you. And customers, by some estimates, mostly don't email — they leave.
Keyword assertions: assert present AND absent
The cheapest, highest-value upgrade is a keyword assertion: after fetching the page, check the body for the presence or absence of specific strings. The pattern that catches the most has two halves:
- Assert a success marker is present. Pick a string that only exists when the page rendered correctly —
Add to carton a product page, your footer copyright, a known<h1>heading. If the marker is missing, the page may have returned 200, but it isn't the page your users need. - Assert failure markers are absent. Alert if the body contains
Fatal error,Warning:,stack trace,database connection, or your framework's default error text. This catches the 200-status error page dead to rights.
Choose success markers with care — they should be stable (not this week's promo banner), specific (not the word "the"), and meaningful (proof the money path works, not just that the header rendered). A monitor asserting on your checkout button is a monitor on revenue.
JSON assertions for APIs
For API endpoints, string matching gets you started, but structured assertions are better. Given a health-ish endpoint:
{
"status": "ok",
"database": "connected",
"queue_depth": 12,
"version": "3.4.1"
}
A useful API monitor asserts on the content: status equals ok, database equals connected, maybe queue_depth under a threshold. Now the failure modes that matter — degraded dependencies reported honestly in the body — fire alerts even when the HTTP layer insists everything is fine. And for the APIs that return errors with status 200, an assertion that the body does not contain an error key is the whole fix in one line.
If your API doesn't have an endpoint worth asserting on, that's the real finding — add a /health route that genuinely checks its dependencies (a real SELECT 1, a real cache ping) and reports them. Five lines of code, and suddenly your external monitor sees what the application sees.
Rendered-DOM checks for JavaScript-heavy pages
Here's the catch with plain keyword checks on single-page applications: the raw HTML response is just a shell — <div id="app"></div> and a script tag. Your success marker, Add to cart, doesn't exist in the HTML at all; it's created in the browser after JavaScript runs. A keyword check against the raw response fails on a perfectly healthy page — or worse, you write the assertion against the shell and it passes on a perfectly broken one.
The answer is a rendered check: the monitor loads the page in a real headless browser, lets scripts execute, and asserts against the resulting DOM — what a user actually sees. It's heavier than a raw HTTP check, so the practical pattern is a fast HTTP check every minute for reachability, plus a rendered content check at a slower cadence for correctness. Between them, both "the server died" and "the JavaScript died" have an alarm attached.
Two real-world shapes to watch for
Two composite examples of how this plays out — both from the family of incidents every e-commerce and SaaS team eventually meets:
- The empty product grid. A retailer's search index fails during a routine reindex. Category pages render beautifully — header, footer, filters — around a grid of zero products. Every page returns 200, response time is better than usual (no products to fetch is fast!), and hard-downtime monitoring stays green through hours of near-zero sales. A single assertion on
Add to cart— or on the absence of "0 results" — turns that into a two-minute detection. - The broken search that "works." The search page loads, the box accepts input, and every query returns the error state — rendered prettily, with status 200. A monitor that requests
/search?q=known-productand asserts a known product name appears in the body is the only external check that catches it.
The common thread: monitor transactions and outcomes, not just endpoints. "Does the homepage answer" is table stakes; "does searching for our best-seller return it" is monitoring that maps to money.
Green should mean working
A dashboard that says "up" when the checkout is dead is worse than no dashboard — it actively reassures you while you lose customers. CompleteStatus lets every HTTP monitor assert on content, not just status: keyword checks for strings that must be present and strings that must never appear, JSON body assertions for APIs, and rendered-DOM checks (via a real headless browser) for JavaScript-heavy pages — all in one dashboard with alerts to email, Slack, Discord, Telegram or webhooks. And since soft failures aren't limited to websites — services on other ports fail softly too, as we covered in TCP port monitoring — assertions belong wherever a "successful" response can still be wrong.
Start free and add one keyword assertion to your most important page today. Pick the string that makes you money — if it ever goes missing, you'll be the first to know instead of the last.