My weekly documentary bot died on its Wednesday run. The video was already published; the crash landed on the last step, playlist assembly.
googleapiclient.errors.HttpError: <HttpError 409 when requesting
https://youtube.googleapis.com/youtube/v3/playlistItems ...
returned "The operation was aborted.">The line right above it said:
[playlists] created: ... PLAF•••••••••Create a playlist, then try to put a video in it, and get a 409.
That picture was familiar. I had published a post about this exact cause the week before: the playlist I just created did not exist. That fix shipped in August, to both bots.
One question for you. That bug you fixed last quarter — is the guard attached to the cause, or to the symptom you happened to see that day?
What got fixed, and what didn't
Here is what the code looked like:
# A freshly created playlist is empty. Listing it returns 404 before propagation.
have = set() if fresh else _existing_items(yt, pid)
for vid in vids:
if vid in have:
continue
yt.playlistItems().insert(...).execute() # bareThe comment is exactly right. "A freshly created one returns 404 before propagation." I knew that, and I skipped the read because of it.
And then on the very next line I write to that freshly created playlist, with nothing protecting it.
A resource that hasn't propagated isn't only missing for reads. It's missing for writes too. Reads say 404, writes say 409 — that is the entire difference. What I should have learned in August was "you cannot touch this resource before propagation finishes." What ended up in the code was "avoid the listing call that returns 404." I took the status code from the incident and left the window behind.
Both forks were carrying the same half
This bot has two forks. Only the niche differs; the engines split from a common ancestor. So I have a habit: fix one, port it to the other. The August 404 fix did land in both.
Which means:
fork A read guarded write bare
fork B read guarded write bareDiffing the two forks against each other shows nothing. They are missing the same thing. Sameness between forks catches "did the fix reach both sides"; it cannot catch "was the fix right in the first place."
I do run a fork-drift check — one invariant line per real incident:
("engine/claude_cli.py", "retry a transient failure once", r"for attempt in \(1, 2\)", None),
("engine/discover.py", "429 is a backoff signal, not a failure", r"Retry-After", None),There was no playlist line in that table. When I fixed the 404 in August I didn't write an invariant — I had hand-ported it to both sides, so I considered it handled.
Two options here
You can wrap the one call that threw and be done. How far would you go?
- Add a 409 retry to
playlistItems.insert. Today's crash stops happening today. - Guard the whole window — "before propagation finishes" — and force that fact onto both forks.
I took 2. The code is short:
TRANSIENT = (404, 409, 500, 503)
RETRIES = (2, 5, 15) # seconds
def _retry(request, what, sleep=time.sleep):
for i, wait in enumerate(RETRIES + (None,)):
try:
return request().execute()
except HttpError as e:
if wait is None or e.resp.status not in TRANSIENT:
raise
sleep(wait)The last two lines are the point. A failure that isn't a propagation delay (a 403, say) is raised on the first try, and exhausting the retries raises too. Swallow the exception here and a video that never got added looks like "assembly complete." Turning a failure into an empty result is something I have already been burned by badly enough that those lines write themselves now.
Then one line into the fork invariants:
("engine/playlists.py", "retry propagation delay (409/404) on insert-after-create (2026-09-09)",
r"def _retry", None),Delete _retry on either side now and the check blocks it.
The check runs without a network
Retry code is hard to verify until you take a real 409 again. So I injected sleep and asserted three things:
- propagation delays (409, 404) are absorbed — succeeds on the third attempt, waits 2s then 5s
- a 403 is raised on the first attempt — permissions don't get papered over by retries
- exhausting the retries ends in failure — no quiet success
[playlists] test 409 - retrying in 2s (1/3)
[playlists] test 404 - retrying in 5s (2/3)
selfcheck OK - 3 retries · transient (404, 409, 500, 503)The honest part: the retry path has not met a real 409 yet. I ran both channels live, but there was no new playlist to create, so the retry branch never executed. Real evidence arrives the next time a new category reaches two episodes. Until then this is "a fix that passes its checks," not "a fix confirmed in the field."
Three things to check on your own code
- Pick one bug you fixed last quarter. Is the guard on the cause, or on the status code you saw?
- Is there another call in that same file where the same cause can surface? (Guarded the read? Check the write. Guarded the list? Check the single fetch.)
- If you keep forks or copies, where do you write the invariant both must satisfy — as opposed to diffing them against each other?
Conclusion
This wasn't a new bug. It was the other half of the one I fixed in August. And the reason that half survived four months is that I took the symptom home from the incident instead of the cause.
The cost was one run. One video's playlist entry, with publishing already done. A small accident. But when a small accident happens at a spot marked "fixed," the thing worth re-reading is how you fixed it.
Do one thing right now: open a file where you recently wrote "this is fixed," and read whether the cause that comment describes shows up again a few lines below it.