
Half of all "but the monitor said it was up" incidents come from the same root cause: a status code of 200 with a broken response body. The web server is happy. The load balancer is happy. The uptime tool is happy. The user is staring at a login page instead of their dashboard.
HTTP status codes describe the transport layer's opinion of the request. They do not describe whether the page or API actually did its job. A 200 response can be:
- A login page served because the session expired.
- An empty JSON array because the database query returned nothing.
- A friendly error template rendered with status 200.
- A stale build because a CDN purge failed.
- A wrong-region or wrong-locale page.
- A maintenance page that did not bother to return 503.
This guide is about catching those false-green outages with response body validation.
For the status-code side of the story, see HTTP Status Codes Explained.
What "False Green" Looks Like
A false green is any check that passes when the user-visible reality is broken. The classic examples:
| Symptom | What the monitor sees | What users see |
|---|---|---|
| Session expired | 200 + login HTML | Cannot reach dashboard |
| DB query empty | 200 + { "results": [] } |
No products listed |
| Stripe error | 200 + { "ok": false } |
Cannot complete payment |
| Stale CDN | 200 + last-week's HTML | Old prices, missing features |
| Wrong region | 200 + German page | English users confused |
| Error template | 200 + "Something went wrong" page | Broken UI |
| Maintenance page | 200 + "Be back soon" | Site down |
| API contract drift | 200 + { "items": [...] } instead of { "data": [...] } |
Frontend crashes |
| Empty cart due to feature flag | 200 + { "items": [] } |
Cannot checkout |
None of these are caught by a simple "is the status code 200" check. All of them are catastrophic for revenue.
The Minimum Response Body Assertion
Every monitored URL should assert at least one of:
- Must contain a phrase that only appears when the page is healthy.
- Must not contain a phrase that only appears when the page is broken.
For a marketing site: Must contain "Get started". If a deploy nukes the hero CTA, the check fails.
For a docs page: Must contain "## Installation". If a build pipeline serves an empty markdown render, the check fails.
For a dashboard route: Must contain "Dashboard", Must not contain "Sign in". If a session monitor breaks, the check fails.
For a JSON API: Must contain '"status":"ok"'. If the handler short-circuits early, the check fails.
The smallest assertion is "this page is not the wrong page." That single rule prevents most false-green outages.
For deeper content change detection over time, see Content Change Detection.
Designing the Assertion
Pick an assertion that is stable and uniquely healthy:
- "Get started" is stable on a homepage that has shipped that CTA for two years.
- A user's name on a dashboard is not stable; it changes per user.
- A current price is not stable; it changes.
- An exception message is not stable; multiple errors look similar.
The assertion should:
- Not change on every deploy.
- Not depend on a logged-in user.
- Not match the broken state by accident.
- Be specific enough that no other page contains it.
Anti-pattern: asserting <html> exists. Every page returns <html>. The check is useless.
Better pattern: asserting "Get started for free" on the homepage. Only the healthy homepage contains it.
JSON Response Validation
JSON APIs are where status-code-only monitoring fails most often. A 200 with an empty array is the most common false green in production.
Validate at three levels:
1. Shape
The response is valid JSON and matches the expected schema.
{
"status": "ok",
"data": { "items": [ ... ] }
}
Assert:
- Response parses as JSON.
- Top-level keys
statusanddataexist. data.itemsis an array.
2. Content
The fields you actually depend on are present and non-empty.
Assert:
status == "ok".data.items.length >= 1(or expected minimum).- A known stable item exists in the array.
- No field equals
nullthat should be populated.
3. Freshness
Where applicable, the response reflects recent state.
Assert:
data.updated_atwithin the last N minutes.data.build_idmatches the expected deploy.data.versionis newer than or equal to the last known good.
This is the JSON equivalent of Must contain and Must not contain and catches almost all of the bad outcomes above.
For database-driven empty arrays, see PostgreSQL Production Monitoring and MySQL Production Monitoring.
Detecting the Login Page
The most common false green: a session expired, the server redirected, and the response body is the login HTML with status 200.
Mitigation:
- Probe with a real test user or API key.
- Assert content unique to the authenticated page: "Welcome back", "Sign out", a known navigation label.
- Assert content that only appears when not signed in is absent: "Sign in", "Create account".
For securing the probe credentials, see Monitor Authenticated APIs.
Detecting Error Templates
Friendly error templates often render with status 200. They contain phrases like:
- "Something went wrong"
- "An unexpected error occurred"
- "We are sorry for the inconvenience"
- "Try again later"
- "Page not found"
- "Service unavailable"
Add a global "must not contain" assertion that lists these phrases across all monitored pages. One regression in error rendering will trip the check on every URL at once.
This is also a good place to assert the absence of debug strings:
Whitelabel Error Page(Spring Boot)NoMethodError,ActiveRecord::RecordNotFound(Rails)Unhandled exception,TypeError: Cannot read properties of undefined(Node)Application Error(Heroku)502 Bad Gateway,503 Service Unavailable(NGINX text rendered with 200 by an outer reverse proxy)
If any of these appear in any monitored body, you have an active production bug.
Detecting Stale Builds
When a CDN purge fails or a service worker caches aggressively, users see last week's build. The site is up. The site is also wrong.
Embed a deploy fingerprint in the HTML:
<meta name="build-id" content="2026-05-26-7f3a">
<meta name="commit" content="abc123">
<meta name="deployed-at" content="2026-05-26T03:04:11Z">
Then your monitor can assert:
build-idmatches the expected current deploy.deployed-atis within the last N hours.
For APIs, return a X-Build-Id header or include build_id in the JSON response. Assert it from the monitor.
Pair this with Sitemap and robots.txt Regression Monitoring for the deploy-safety story.
Wrong-Region and Wrong-Locale Detection
Multi-region or multi-locale sites can serve the wrong content with a 200 status:
- US user reaches an EU CDN node and gets EU pricing.
- An English visitor gets the German page because of a misconfigured locale fallback.
- A maintenance window in one region routes to the wrong origin.
Run monitors from multiple regions and assert:
- Page contains the expected currency symbol or locale string.
- Page does not contain the wrong locale's strings.
- Region-specific URLs (
/eu/,/de/) resolve to the expected region.
A "Must contain {{content}}quot; probe from a US region catches an accidental EUR rollout. A "Must contain English-only phrase" probe catches locale regressions.
Detecting Empty States That Should Not Be Empty
For collection pages and search results, empty is sometimes valid. Often, it is not:
- Homepage product grid should never be empty.
- Search for a known SKU should never return zero results.
- A "Latest posts" page should always have at least one item.
Assert the presence of a known stable item or a minimum count. See Site Search Monitoring for the search-specific version of this pattern.
Headers Are Bodies Too
Several false-green outages hide in headers, not bodies:
Content-Type: text/htmlreturned for what should beapplication/jsonafter a routing change.Set-Cookiemissing theSecureflag after a config drift.Cache-Control: max-age=31536000on a personalised page.Location:redirect chain that ends at a login page.- Missing
Strict-Transport-Securityafter a deploy.
Treat headers as part of the response contract. Assert the headers you depend on. See HTTP Security Headers Monitoring.
Beyond Status: Combine With Latency and Synthetic
Content validation is not the only signal. Combine it with:
- Latency - a 200 that takes 12 seconds is still a failure.
- Synthetic flows - log in, browse, add to cart, checkout. Each step asserts content.
- Real-user monitoring - confirms what synthetic checks suggest, see Synthetic vs RUM.
- Health endpoints - assert
livez/readyzcontents, not just status, see Health Check Endpoint Design.
The combination catches "fast and wrong", "slow and right", and "looks correct but broken downstream" - all of which a status-code check alone misses.
Practical Assertion Library
A starter set of assertions for common page types:
Homepage
- Must contain:
Get started, primary CTA text - Must not contain:
Application Error,Something went wrong, debug strings - Status: 200
- Latency: under 1500ms
Pricing page
- Must contain: current price string,
Start free, plan names - Must not contain:
null,undefined, error strings, last-quarter's price - Status: 200
Dashboard (authenticated)
- Must contain:
Sign out, user-agnostic dashboard nav label - Must not contain:
Sign in,Create account - Status: 200
Status page
- Must contain:
Operational(when healthy) - Must not contain: stale
last-updatedvalue older than N minutes - Status: 200
Public JSON API
- Must contain:
"status":"ok", expected top-level keys - Must not contain:
"status":"error","message":"Internal Server Error" - Status: 200
- Latency: under 800ms
- Response parses as JSON
Webhook receiver healthcheck
- Must contain:
"ok":true - Must not contain:
signature_invalid - Status: 200
- TLS expiry: not within 14 days
See Incoming Webhook Monitoring for the webhook story end to end.
Alerting Thresholds
Critical
- Critical URL returns 200 with wrong content (login page, error template, stale build).
- Authenticated endpoint stops returning authenticated content.
- Pricing/checkout page returns empty or null fields.
- JSON API contract breaks (top-level keys missing).
- Build fingerprint stale beyond expected deploy lag.
High
- Optional fields missing from API response.
- Latency drift on otherwise-passing content checks.
- Locale-specific content mismatch in a single region.
- New error-template phrase observed in any monitored body.
Informational
- Content changes detected on otherwise stable pages.
- New build fingerprint observed.
- Response shape change with no field loss.
For making sure these alerts get acted on, see Alert Fatigue.
Response Body Validation Checklist
- Every monitored URL has at least one content assertion
- Authenticated pages assert authenticated markers
- Public APIs validate JSON shape and key fields
- Global "must not contain" list for error templates and debug strings
- Build fingerprint in HTML and JSON, asserted by monitors
- Locale and region-specific assertions where relevant
- Empty-state assertions for pages that should never be empty
- Headers (Content-Type, Cache-Control, security headers) validated
- Latency thresholds combined with content checks
- Alerts distinguish content failure from connection failure
- Runbook entry per content-assertion alert
The fewer assertions per URL the better, as long as each one is uniquely healthy. Five focused assertions catch more outages than fifty noisy ones.
How Webalert Helps
Webalert is built to catch false-green outages that status-code-only monitors miss:
- HTTP monitoring - Probe any URL with method, headers, body, and timeout you control.
- Content validation - Multiple
Must containandMust not containrules per check. - JSON path assertions - Walk into the response body and assert fields, types, and values.
- Header assertions - Validate
Content-Type, security headers, cache controls. - Build-fingerprint checks - Assert the deployed
build-idorversionmatches expectation. - Multi-region checks - Catch wrong-region and wrong-locale incidents.
- Authenticated checks - Bearer tokens and custom headers for protected routes.
- Latency alerts - Combine fast-and-wrong detection with slow-and-right detection.
- Status pages - Surface "200 but wrong" incidents to customers honestly.
Example Webalert check:
- URL:
https://example.com/dashboard - Method:
GET - Headers:
Cookie: <test-session> - Expected status:
200 - Must contain:
Sign out - Must not contain:
Sign in,Something went wrong,Application Error - Response time: under 2000ms
- Region: US + EU
A second check on the same URL can assert the build fingerprint:
- Must contain:
<meta name="build-id" content="2026-05-26
When that build assertion fails the day after a deploy, you know the CDN did not purge.
For the related API Uptime story, the same pattern applies: never trust status alone.
Summary
HTTP 200 is necessary but not sufficient. The most painful production incidents - login pages served on dashboards, empty JSON on critical APIs, stale builds, wrong regions, friendly error templates - all return 200.
Add response-body assertions to every monitored URL. Validate JSON shape and key fields. Assert build fingerprints. Watch for known bad phrases. Combine with latency and synthetic checks. Done well, "the monitor said it was up" stops being a postmortem line.