Stop an outage from spending a job's last attempt - #66
Merged
Conversation
Between 2026-08-26 and 08-28 every model provider ran out of budget at the same time. Site extraction is a model call, so every crawl failed, and each failure was charged to the job as if the payload were at fault. 4,888 jobs burned through `max_attempts` and became `failed`, which nothing ever brings back. When the chain recovered the queue did not, because the outage had also consumed the work that regenerates work: every `refresh_research` card had been approved, so `workspacesWithInternalBacklog` found nothing, queued no crawls, and the pipeline sat at a fixed point it could not leave. The daily digest reported zero crawled, zero found and zero sent, all of it true. `failJob` now takes an optional check for "this failure was not this job's fault". When it matches, the job goes back to pending with its attempt refunded and a flat ten-minute hold, and can never reach `failed`. Refunding is safe here in a way it is not in `reclaimStalled`: there the payload is the suspect, here every other job in the queue is failing identically, which is what makes the payload innocent. `drainQueue` stops the tick on the first deferral rather than asking a provider that has already said no twenty-four more times, and the server wires in `isBudgetExhausted` and says plainly that it is holding. The previous version of this outage was silent apart from a 400 in `jobs.last_error` that read like a bug in the crawler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017o2oNWB7bwNxNsaRB7oTTY
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The incident
Between 2026-08-26 and 08-28 every model provider ran out of budget at once — OpenAI credits at zero, Anthropic on the org spend cap (reported as a 400, not a 429). Site extraction is a model call, so every crawl failed:
Each failure was charged to the job as if the payload were at fault. 4,888 jobs burned through
max_attemptsand becamefailed, which nothing ever brings back.When the chain recovered, the queue did not — because the outage had also consumed the work that regenerates work. Every
refresh_researchcard had been approved, soworkspacesWithInternalBacklogfound nothing and queued no crawls. The pipeline sat at a fixed point it could not leave, and the daily digest reported zero crawled, zero found, zero sent — all of it true.The fix
failJobtakes an optional check for "this failure was not this job's fault". When it matches, the job returns topendingwith its attempt refunded and a flat ten-minute hold, and can never reachfailed.Refunding is safe here in a way it deliberately is not in
reclaimStalled: there the payload is the suspect, and the evidence is that the job killed its container. Here every other job in the queue is failing identically, which is exactly what makes the payload innocent.Two supporting changes:
drainQueuestops the tick on the first deferral rather than asking a provider that has already said no another 24 times.isBudgetExhaustedand says plainly that it is holding. The previous version of this outage was silent apart from a 400 injobs.last_errorthat read like a bug in the crawler.The predicate is injected rather than imported, so
queue.tskeeps knowing nothing about what a job actually does.Tests
Three new cases in
queue.test.ts, all verified to fail without the fix:"dead"(the exact production behaviour)bun test: 1,445 pass, 0 fail across 96 files.bun run typecheckclean, prettier clean.Note on the live backlog
The 828 distinct domains killed by this incident were already requeued by hand in prod on 2026-08-29 (ids in
~/backups/outreachgraph-requeued-jobids-2026-08-29.txt), and the pipeline is generating its own work again. This PR is the fix that stops it recurring — no data migration needed.🤖 Generated with Claude Code
https://claude.ai/code/session_017o2oNWB7bwNxNsaRB7oTTY