← Back to blog

Writing assertions that catch a "200 OK" outage

2026-09-06

guidehttp-checks

The most expensive outages are the ones your monitoring agrees with.

A load balancer that serves a blank page when every backend is gone. A CDN happily returning a cached copy of your API from an hour ago. A single-page app whose server ships index.html for every route — including the broken ones — because routing errors is the client's job. A maintenance page that someone, reasonably, configured to return 200 so browsers wouldn't scare users.

All of these answer the phone. All of them say "200 OK". None of them are your service working. If your check only asks did something respond?, the dashboard stays green while your users file tickets — which is worse than no monitoring, because now you have evidence you were fine.

The fix is to stop checking that the service answers and start checking that it works. That's what assertions are for.

What a bare check actually proves

In okokumo, a check with no assertions passes when the request completes with any status below 400. That's a deliberate floor, not a recommendation: it proves DNS resolved, TCP connected, TLS handshook, and something spoke HTTP back. Useful facts. But every failure mode in the opening paragraph sails through it.

Each assertion you add is a claim the response must prove. okokumo has four kinds, and they compose — a check passes only if every assertion passes, and when one fails, the incident records which one, with the actual value we saw next to the value you expected.

Layer 1: assert the exact status code

{ "type": "status_code", "value": "200" }

This looks redundant — didn't the bare check already handle status? No: the bare check accepts anything under 400, and that range hides real problems. A 301 to a login page is "under 400". So is a 204 from an endpoint that's supposed to return data. Asserting 200 exactly means an unexpected redirect chain or an empty-response regression flips the check.

One subtlety worth knowing: our probes follow redirects (up to ten), and assertions run against the final response. So if your site 301s from apex to www, assert against what the end of that chain returns — or point the check at the final URL and assert 200 so a new, unexpected redirect gets caught.

Layer 2: assert the body says something only a working service says

This is the layer that catches the "200 with garbage" class, and it's the one most checks are missing.

{ "type": "body_contains", "value": "\"status\":\"ok\"" }

The rule: pick a string that can only appear when the request was actually served by your application, from live data. Good choices:

  • A field from your health endpoint's JSON: "database":"connected".
  • A footer string rendered by your templating layer — if the template engine ran, the app booted.
  • For a product page: the product's name. A cache serving a deleted page or an error template won't contain it.

Bad choices: <html> (every error page has it), your company name (it's on the maintenance page too), anything your CDN's default error template also contains. If the string would survive on the "we'll be back soon" page, it proves nothing.

An empty body fails any body_contains assertion by definition — which is exactly the blank-page-from-the-load-balancer case. We read up to 1 MiB of the response for matching, so assert against something near the top of the page if your payloads are large.

Layer 3: assert latency, because slow is the first symptom

{ "type": "max_latency_ms", "value": "800" }

A service at 95% capacity doesn't return errors — it returns 200s, slowly, and then it returns nothing. A latency assertion turns "slow" from a thing you notice in retrospect on a graph into a thing that fails the check while there's still time to act.

Set the bound from your own data, not from hope: open the check's latency chart, look at a normal week, and set the assertion at two to three times the typical value. Too tight and you're paging on network weather; too loose and it only fires after users already noticed. And remember the check timeout still backstops this — the assertion catches degraded, the timeout catches dead.

Layer 4: assert a header when the body can lie

{ "type": "header", "name": "Content-Type", "value": "application/json" }

Headers catch failures that body matching can't see cleanly:

  • Content-Type — your API endpoint returning text/html means something upstream (a proxy error page, a captive portal, a misconfigured route) answered instead of your app.
  • A build or version header — if your app sets X-Release, asserting it exists proves the response came from your application and not from infrastructure in front of it. In okokumo, a header assertion with a name and an empty value passes if the header is present at all, whatever its value — presence is often all you need.
  • Cache freshness — if stale-cache-as-outage is your risk, assert on a header your origin sets and your CDN forwards, so a response served without ever reaching the origin fails.

Putting it together

A check that survives the "200 OK outage" looks like this:

{
	"type": "http",
	"name": "API health",
	"url": "https://example.com/health",
	"interval_seconds": 60,
	"assertions": [
		{ "type": "status_code", "value": "200" },
		{ "type": "max_latency_ms", "value": "800" },
		{ "type": "body_contains", "value": "\"database\":\"connected\"" },
		{ "type": "header", "name": "Content-Type", "value": "application/json" }
	]
}

Four claims: it answered correctly, fast enough, with content only the live app produces, in the shape the app produces it. Each layer catches a failure the others miss.

When one fails, you're not left guessing. The incident history records the reason — assertions failed: body_contains — and the execution log keeps each assertion's expected and actual values, so 4am-you can see "expected 200, got 503" without opening a terminal.

Two honest caveats. First, assertions run per-request; they don't replace end-to-end tests, and a body_contains on your homepage won't tell you checkout is broken — put a check on the endpoint you actually care about. Second, a failing assertion goes through the same confirmation as any other failure: the consecutive-failure threshold and second-region confirmation still apply, so a tight latency bound won't page you over one slow probe.

Start with the status code and one body string. That pair alone catches most of the outages a bare ping never will — and it takes about a minute to add to an existing check.


okokumo is infrastructure monitoring hosted in France — HTTP, heartbeat, TLS and domain checks, probed from two EU regions. Assertions are available on every plan, including Free.