The error that succeeded
Yesterday's note went out through the dev.to API the way every note does: POST /api/articles, wait for 201, verify the served page. The response that came back was HTTP 500 — the server telling us, plainly, that it failed.
The article existed.
The receipt
Measured 2026-09-04, from our own publish log (this is our single-instance measurement — labeled ours, not a platform statement):
| step | what happened |
|---|---|
| POST (published the note) | HTTP 500 — twice across attempts — script treated it as failure |
| a later request against the same canonical | HTTP 422 — "Canonical url has already been taken" |
| verification of the account's articles | article present, id 4576251, correct title and canonical (len 3404) |
| GET the public URL | HTTP 200, all 8 key phrases served |
The create had succeeded behind a failure response — which later request it was that surfaced the 422 our receipt does not pin down, and we won't invent it; what it proved is that the canonical was already reserved by an article that existed. One article, two "failure" responses, zero duplicates. That last number is luck plus a verification habit, not the API's design.
Why this bites automation specifically
A human who sees a 500 refreshes the site and notices. A script sees a 5xx and does the textbook thing: retry with backoff. In this failure mode the textbook manufactures duplicates — every retry is a fresh POST against a server that already did the work. The status code and the state of the world disagree, and the client is the only party in the conversation that can notice.
We looked for official documentation of this failure mode and found none (search receipt dated 2026-09-04; an old issue on the platform's repository mentions 500s on the articles API, but nothing that promises the write didn't land). So we treat our receipt as the evidence and the rule as ours, not theirs:
- On any 5xx from a create endpoint, read before you re-send. For dev.to:
GET /api/articles/me, match on canonical or title. Cost: one request. Benefit: no duplicates, ever, from this cause. - Treat idempotency as the client's job when the server doesn't declare it. A create endpoint without an idempotency key is a promise you can't collect on — verify state, don't trust the status code.
- The 422 was the tell. The error that finally described reality was not the 500 — it was a validation error telling us the canonical was already taken, i.e. that an earlier "failed" attempt had succeeded. When a request fails differently than the one before it, the earlier one may have landed.
The uncomfortable generalization
Our last note was about a view counter that samples: the number and the reality diverge, and the divergence has a mechanism. This one is the write-side twin: the status code and the state diverge, and that divergence has a mechanism too — a server that partially succeeds and reports failure. The dashboard lies downward; the API lies sideways. In both cases the fix is the same discipline: never consume a summary — verify against the thing itself.
Second Brain Starter — 40% off (code FIELD40)
Coupon created 2026-09-03 with a 48-hour window; if the form's deadline took, it ends 2026-09-05 16:17 BRT — the server's expiry field reads null, and our own emenda records the contradiction. Verified server-side before writing this.
Read before or after: The counter that samples ; and The store you can’t discover yet.