The 200 OK error: when a success response carries a failure
Definition, why it is invisible by design, and a measured experiment on the defaults · updated 2026-08-18 · no affiliate links on this page
HTTP 200 asserts that a message arrived and the server answered - nothing more. Whether the operation actually happened is stated inside the response body, and many real-world APIs report failures there while the status stays 200. Automation platforms evaluate step success on the HTTP status, so a success-wrapped failure enters your run history as a green checkmark: the transport layer worked, the business operation failed, and nothing anywhere disagrees with the word "success".
This failure class sits in a blind spot that is architectural, not accidental: the platform genuinely cannot know what a failure looks like inside an arbitrary API's response schema without being told. That makes it different from the failures on our reliability scoreboard - it is not the platform dropping your run, it is the platform faithfully recording a delivery whose content was a failure notice. The run log is telling the truth about the wrong layer.
Where it bites
- APIs that wrap errors in 200s. Payment processors returning declines as structured 200 responses, CRMs answering 200 with a validation-errors array, bulk endpoints that accept a batch and report per-record failures in the body. The workflow shows green; the payment declined, the contact was rejected, three records of ten were skipped.
- Schema drift. The destination API changes a field name or response shape; calls keep returning 200 while writes quietly stop landing where your downstream logic expects them. Nothing errors - the payload is simply wrong now.
- Soft limits. Some services throttle by accepting requests and deferring or discarding work rather than answering 429 - the polite cousin of the quota wall we measured on the reliability page, where accepted stopped meaning delivered.
The experiment (EP3), measured
Content about this failure class is plentiful; measurement of it was nonexistent - so on August 13 we pre-registered a design on this page, and on August 14-18 we ran it. Our receiving endpoint answered experiment-tagged events with a real HTTP 200 carrying the body {"ok": false, "error": "schema-drifted"} - a genuine success-wrapped failure - while still recording the receipt, so delivery stays provable and only the response body reports failure. We fired these through the identical webhook→HTTP workflows we always run, all platform settings at their defaults. These runs are excluded from the headline Silent Failure Rate; experiments never share a denominator with normal operation.
What the defaults recorded
| Zapier | n8n (self-hosted) | |
|---|---|---|
| Runs with a 200-OK error body | 3 / 3 | 3 / 3 |
| How the run was recorded | Successful (green) | Success (green) |
| Was the error visible anywhere? | Yes - in the step's output data, if you open it | Yes - in the HTTP node output, if you open it |
| Counted against the platform's own failure metric? | n/a | No - included in the dashboard's "0% failure rate" |
| Billed? | 1 task each - the same as a success | $0 (self-hosted) |
The blind spot is not that the platforms can't see the error - both captured the full error body and show it if you open the step. It's that they don't look: success is decided on the HTTP status before anything reads the body. Make's documented behaviour is the same; its paid window in our harness has closed, so it is not in the measured table.
The cost of seeing
We then measured what it costs to catch these. On Zapier we added one response-body assertion (a Filter checking the success field) plus an alarm step, and fired one healthy event and one error event:
- Healthy event: the Filter stopped the run - 1 task (the delivery step only; the Filter itself bills nothing).
- Error event: the Filter passed and the alarm fired - 2 tasks(delivery + alarm), and the alert email arrived.
So the standing cost of looking is $0: a response assertion adds a billable step only on the runs where it actually catches a failure and does something about it. Seeing is essentially free; it is the default of not looking that quietly turns your failures into successes. (The task meter moved exactly as predicted - 275 to 290 across the whole experiment, no phantom charges.)
What to do about it today
Assert on the body, not the status: one added step that checks the response's success field (or the returned ID you expect) converts this silent class into a loud one, and - as we measured above - it costs nothing until it actually fires. And because some failures produce a perfectly-shaped success body with no write behind it, keep the end-to-end net too: read-after-write on the records that matter, and daily counts-in versus counts-out - the same reconciliation discipline behind every number on our scoreboard.
FAQ
Can a webhook or API call fail even if it returns 200 OK?
Yes, and it is one of the most common silent-failure shapes in production automation. HTTP 200 only asserts that the request arrived and the server produced a response - the transport layer worked. Whether the operation succeeded is stated inside the response body, and plenty of APIs report failures there ({"success": false}, {"status": "error"}, a 200 with an empty result) while the HTTP status stays 200. Any system that equates 2xx with done - which is the default behaviour of automation platforms - records these failures as successes.
Does Zapier detect an error inside a 200 response?
Not by default - we measured it. We sent Zapier three genuine 200 responses carrying an error body; all three were recorded as Successful (green), each billed 1 task like a success, and the error was visible only if you open the step's output. Self-hosted n8n behaved identically (and counted the runs in its dashboard's 0% failure rate). Catching it requires an explicit step you add yourself - a Filter on the response's success field - which we also measured: it costs $0 standing (the Filter is free) and adds a billable step only on the runs where it actually catches a failure and triggers an alert. Full results and method are in the experiment section on this page.
How do I catch 200-OK errors in my workflows?
Two complementary layers. In-workflow: add a response assertion after the call - check the body's success field, or the presence of the ID you expect back, and route failures to an error path. This costs one extra step (billable on per-task platforms) and catches structured error bodies. End-to-end: read-after-write verification and daily count reconciliation - fetch what you just wrote and compare the fields that matter, and compare records-in against records-out on a schedule. The second layer is the only one that also catches the case where the API said success, returned 200, and still wrote nothing.
Related
The live Silent Failure Rate scoreboard · Silent failure vs rejected webhook · Uptime is not your failure rate · Webhook retry semantics, measured