Build1 publisher3 min readPublished
A forty-line guard removes the second writer behind a double-posted announcement
The idempotency key was correct and sitting in a git file that had not been pushed, so a second run repeated the whole job. The team weighed a post-hoc duplicate reaper against a guard, and shipped the guard.
The Engineer · Build desk

What happened
- On 2026-08-31 an agent posted the same dev.to announcement to Bluesky twice, from runs on two machines that started 57 minutes apart, both after code that had already checked for duplicates.
- The local run wrote the reservation id into a git-tracked stock file, and the retry cron read origin/main at 08:43, found no id, and created a second scheduled article and a second announcement.
- That evening's commit moved the duplicate check onto Bluesky itself, fetching up to 100 posts of the account's own feed and matching the exact announcement text before posting anything.
- The deferred alternative was a reaper that re-searches the feed after posting and deletes every copy but the oldest, estimated at one to two hours of CLI work and tests.
- Six days after the incident the team shipped a guard of about forty lines that throws unless the CLI is running inside GitHub Actions.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- decision The zero-recurrence count measures a job that fires a couple of times a day. Anyone running the same pattern per event enters the seconds-wide window often enough that the reaper's estimate is the cheaper of the two.
- cost The guard's cost falls on whoever wants to run the path by hand: dispatch the workflow, wait on the run, and give up reproducing the side-effect path on a laptop.
- exposure Refusing to post when the feed lookup fails turns a Bluesky read outage into a missing announcement that waits for the next scheduled run.
- constraint The seconds-apart collision stays possible by design, so correcting the timeline still depends on a person noticing a duplicate that no code will remove.
The reservation mark worked as designed: a row carrying a dev.to article id means the work is done [8]. The problem was where that row lived. The local run wrote it at 07:46 UTC and the push did not happen until 09:25, so for 99 minutes the only copy was on one machine's disk [7][9][24]. The retry cron's read landed 42 minutes before that push [25].
The second writer was there on purpose. GitHub's cron does not fire on time under load, and on some days theirs ran two to four and a half hours late [5]. The health check, as written, advised the agent to run the same CLI locally when an announcement was missing, and the agent followed the advice [6]. The write-up's summary of the result: "The idempotency key was correct. It was just sitting in a state that only one writer could see." [11]
The evening commit made the platforms authoritative: before creating an article, the publisher fetches the account's full list, drafts and scheduled included, and refuses on a title match [12]. If the Bluesky lookup itself fails, the CLI does not post [14]. The stated reason is that "Leaving the announcement for the next run is cheaper than a possible duplicate" [15].
Read-then-write has a window. Between the read and the write sit the round trip to Bluesky and however long a new post takes to appear in the author feed. Two writers inside that window both read an empty feed and both post [16]. A reader of the original incident write-up argued that when the write side offers no conditional write, the read should not be treated as authoritative. The alternative that reader offered was to make duplicates visible and reversible [17].
That suggestion went into the ledger as a deferred item with a plan: re-search the feed after posting, delete all but the oldest copy, record each deletion in the post log, and expose the detected-duplicate count in the health check [18]. The delete call already existed in the Bluesky client [19]. The ledger also recorded a recurrence count of zero since the read-before-write check went in [20]. The recorded impact was one duplicate sitting on the timeline until a human removes it, in a window measured in seconds to tens of seconds, on a job that runs a couple of times a day [21].
For that zero to transfer, the firing rate has to transfer. A path that runs per event enters the same seconds-wide window many times an hour, and the reaper would remove duplicates the guard cannot prevent.
The guard tests one environment variable, and it runs before any side effect in the CLI. It returns when GITHUB_ACTIONS is 'true'. Otherwise it throws an error, written in Japanese, telling the operator to start the workflow with gh workflow run and wait on it with gh run watch [22][23]. What it removes is one writer, the local session. Both crons, the 02:17 primary and the 08:33 retry, run inside Actions and pass its only test [4]. The write-up does not describe a concurrency control that would keep a badly delayed primary run from overlapping the retry [27].
What to watch
- Whether the deferred reaper ships if the announcement path ever fires more often than a couple of times a day.
- Whether the health check advice is rewritten, now that its suggestion to run the CLI locally hits a guard that throws.
- Whether the workflow gains a concurrency setting so a late 02:17 run cannot overlap the 08:33 retry.