HTTP 200 OK is a statement about a request, not about a page

30/07/2026 · Laurent HTTP status codes
HTTP 200 OK is a statement about a request, not about a page

Read what the specification actually says, because the word OK does a lot of unearned work. RFC 9110 defines 200 as: the request succeeded, and the payload is a representation of the target resource. That is the entire promise.

Every part of that sentence is about the transaction. A server received something it could parse, decided it had a resource for it, and sent bytes. Nothing in there concerns whether the bytes are the page, whether the page has content, whether the content is this year's, or whether a human looking at it would say the site is working.

Uptime, as most tools report it, is the percentage of checks that produced a 200. So it is a measure of how often a server was willing to talk. That is worth knowing. It is not what anyone thinks they are reading.

What a 200 proves, and what it does not

ProvedNot proved
DNS resolved to an addressThat it resolved to your address
A TCP connection was acceptedThat the machine accepting it is the one you deployed to
TLS negotiated, so the certificate is valid todayThat it will be valid next week
Something parsed the requestThat the application ran
Bytes came backThat there is content in them
The server considered the response a successThat the page is the right page, in the right language, with the right price

The second column is where client sites live. None of those failures produces an outage, an alert, or a mark on a report.

Six ways to be broken at 200

These are not hypotheticals. Each one has cost somebody a client relationship, and each one reported perfect availability while it happened.

The page renders without its stylesheet. A build wrote assets to a path the template does not reference, or a CDN rule started returning 404 for one directory. The HTML is intact and the browser stacks every element in one column at full width. Machine-readable, visually catastrophic, status 200.

A fatal error is printed into the body. Some hosts and some configurations return a database error or a PHP fatal with a success status, because the error happened after the headers were sent. The response is a success carrying a stack trace.

The listing is empty. A feed import failed, a filter broke, a plugin lost its post type during an update. The template renders correctly around nothing. This is the version clients notice last and are angriest about, because the site looks deliberate.

The wrong site answers. A DNS record points at an address that now hosts somebody else, or a server's default virtual host catches the request because the intended one failed to load. You get a 200 from a parking page, a hosting welcome screen, or another client's project.

A missing page says it is missing, politely, at 200. The soft 404, which is common enough and damaging enough to have its own article.

The page is a staging copy. A deploy pointed production at the wrong document root. Everything works. Everything is last month's, and the noindex tag from staging came with it.

And the reverse, which matters just as much

If 200 is not proof of health, non-200 is not proof of illness, and a monitoring setup that treats every other code as an outage will train you to ignore it.

A staging site behind HTTP authentication answers 401 permanently, and that is the protection doing its job. The event worth an alert is the day it answers 200. A site in a planned maintenance window should answer 503 with a Retry-After, and the event worth an alert is the 503 that is still there an hour after the window closed. A rate limiter returning 429 to your own probe is telling you about your schedule, not about the site.

Three perfectly normal codes, three alerts that will fire at three in the morning for no reason, three teams that learn to swipe the notification away. Alert fatigue is not caused by too many incidents. It is caused by alerts that were never incidents.

Checking the page instead of the transaction

The change in practice is small and it is mostly a question of what you decide to assert.

Pick, per site, one string that only appears when the page has done its job. Not the site name, which is in the template and survives every failure above. The price block on the pricing page. The first product card in a listing. The submit button of the contact form. If that string is absent, the site is broken regardless of the number on the first line.

curl -sS https://example.com/pricing | grep -qF 'data-price' || echo BROKEN

Two more assertions cost nothing and catch the rest. Response size, compared with the last known good value, catches the empty listing and the stripped stylesheet. Certificate expiry, read at connection time, catches the outage you can schedule around instead of waking up for.

Our own engine does this by default rather than as an option, and since it is our product here is the checkable version rather than the claim: it looks for text patterns meaning a database or fatal error was printed into the page, and for layout signals meaning the stylesheet did not arrive. Both are countable in the public repository. 41 signatures in src/Check/Database.php, 9 signals in src/Check/Css.php. You can also run an address through it here and read what comes back.

The honest cost of doing this: content checks produce false positives that status checks never will. A copy change, a seasonal banner, an A/B test can all move the string you asserted on, and you get an alert about nothing. That is a real price and it is why most tools do not do it by default. It is also a price that pays for itself the first time a client's shop sells nothing for two days at one hundred percent availability.

The codes that are worth reading, once you accept that reading them is not enough, are the subject of the nine that matter in production.

Share this post.
Stay up-to-date

Subscribe to our newsletter

Don't miss this

You might also like