Skip to content

Build1 publisher3 min readPublished

Treating a publish timeout as failure is how a retry can send the same article twice

A dev.to postmortem works through what a publishing runner has to record when the browser stops answering between the click and the proof, because an ambiguous timeout leaves open whether the article went out.

The Engineer · Build desk

Illustration accompanying Treating a publish timeout as failure is how a retry can send the same article twice

What happened

  • On September 11 a solo publishing runner's local ledger recorded 29 articles while the public profile showed 30, and the operator could not say whether rerunning the queue would publish a duplicate.
  • An article published on September 4 went out fine, but the call adding it to its series returned HTTP 500, leaving one article public and one setting unapplied.
  • The author replaced the published boolean with six states, including submission_started, metadata_incomplete and verification_pending, and split content identity from metadata such as series and tags.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Until the destination has been queried, there is nothing to reconcile against, so minimum-interval gates and content fingerprints cannot decide whether a retry is safe.
  • decision A record that cannot express public-but-incomplete forces every ambiguous run into one of two wrong moves: silently accept a missing setting, or resubmit a live article.
  • exposure Readers absorb the cost of guessing wrong, because a bad retry delivers the same article to the same subscribers a second time.
  • capability With a state for a success discovered after the fact, resuming an interrupted run becomes an observation step and the platform is never submitted to twice.

A field named success has two values, and on September 4 both were wrong. The article published; the call that added it to its series returned HTTP 500 [9]. True would have hidden the missing series. False would have invited the runner to publish the whole article again [11]. The author repaired the series two days later, on the article that was already public [10].

The new record distinguishes six outcomes where the boolean distinguished two [22]. Under it, the September 4 run goes submission_started to published_verified to metadata_incomplete, and the September 6 repair moves only the last component [14]. Content identity stays separate from placement. Title, opening fingerprint, body hash and intended account identify the article; series, tags and description are metadata attached to that identity, so a metadata failure cannot erase a verified content receipt [13].

The original pipeline picked a topic, checked a minimum interval, opened the editor, published, verified the page, then appended a record to a local history file, with verification before the record of success [5]. The model behind it was a straight line: draft, publish, verify, record [6]. The running system had at least three independent histories: the editorial rotation that chose topics, the public list the platform maintains, and the browser session including unfinished editor state [7].

"I had not built a queue. I had built a small distributed system and named it a queue because the friendlier word let me ignore the failure modes," wrote the author, who publishes on dev.to as simple_memo [15][20]. The recovery rule follows from one line about timeouts: "A timeout is only evidence that I stopped receiving evidence" [16]. A run recorded as failed can be retried; a run recorded as unknown gets observed first [17].

The September 11 gap came from an article published on June 16 through a separate recovery path, public on the platform and deliberately absent from the rotation ledger because it consumed no scheduled topic slot [3]. Both counts were correct inside their own definitions [4]. The post does not report any article going out twice [26]. The live hazard was the editor: opening the new-post page could restore old article text, and a recovery run that read editor content as the next draft would push old state to a new public URL [8].

For this to transfer, the destination has to answer a query about content you control. Here published_verified means a public URL matching the title, opening, tags and author [27]. Where a platform exposes no lookup keyed to fields like those, verification_pending has no exit and the runner is guessing again. Time gates and content fingerprints are useful only after the public destination has been reconciled [18].

"I now optimize for an interrupted run being boring to resume, not for a clean run being elegant," the author wrote [19].

What to watch

  • Whether the author publishes the reconciliation query itself, including which fields it matches on the platform side.
  • Whether the dev.to new-post page keeps restoring stale editor buffers, since that is the path that could publish old text to a new URL.
  • What the ledger shows the first time a genuinely ambiguous timeout drives a run into verification_pending and out through recovered_existing.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories