Start Debugging

Fix: "API Error: Repeated 529 Overloaded errors" ends a long headless Claude Code run

A claude -p run on Opus with an API key gives up after three consecutive 529s, about 1.5 seconds of backoff, no matter what CLAUDE_CODE_MAX_RETRIES says. Add --fallback-model, set CLAUDE_CODE_RETRY_WATCHDOG=1 with a max wait, or widen the 529 backoff with the OVERLOADED_RETRY delay variables. Checked against Claude Code 2.1.309.

If a long claude -p job dies with API Error: Repeated 529 Overloaded errors, raising CLAUDE_CODE_MAX_RETRIES will not help: on an Opus or Fable model with an API key, Claude Code stops at the third consecutive 529, which with the default 500 ms base delay is about 1.5 seconds of backoff. Pick one of three fixes. Pass --fallback-model sonnet so the third 529 switches models instead of failing. Set CLAUDE_CODE_RETRY_WATCHDOG=1 (plus CLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS) so an unattended run waits out the overload. Or, on 2.1.292 and later, raise CLAUDE_CODE_OVERLOADED_RETRY_BASE_DELAY_MS and CLAUDE_CODE_OVERLOADED_RETRY_MAX_DELAY_MS so the retries you do get are spread over minutes instead of seconds. Everything below was checked against Claude Code 2.1.309 (the current release on October 23, 2026), its error reference and environment variable reference, and the retry code in the 2.1.309 CLI bundle.

The error in context

API Error: Repeated 529 Overloaded errors. The API is at capacity

The rest of the line says the condition is usually temporary and points you to status.claude.com.

In an interactive session you would see the spinner count Retrying in Ns · attempt x/y, maybe a hint like Opus is experiencing high load, please use /model to switch to Sonnet, and you would just switch. In a headless run nobody is watching. The run prints the message as its result, exits non-zero, and the nightly refactor, the issue triage cron, or the CI review step is simply gone. With --output-format json the final result message carries "is_error": true and "api_error_status": 529.

Two facts frame the fix. A 529 is not your rate limit: the error reference says it “is not your usage limit and doesn’t count against your quota”. It means the API is at capacity for that model across all users. And capacity is tracked per model, so a different model often answers while the first one is overloaded.

Why the run gives up so quickly

Claude Code’s documented default is 10 retries with exponential backoff. That is true for most transient errors, but 529 has its own rule. The env-var reference puts it plainly: “with an API key on an Opus model, it stops at the third 529 and shows Repeated 529 Overloaded errors. If you set a fallback model, Claude Code switches to that model rather than showing the error.”

The 2.1.309 bundle shows the full condition. Claude Code counts consecutive 529s when any of these is true:

When the counter reaches 3, it either throws a fallback switch (if a fallback model is set) or ends the request with Repeated 529 Overloaded errors. The only things that skip the early stop are a sandboxed build and CLAUDE_CODE_RETRY_WATCHDOG. CLAUDE_CODE_MAX_RETRIES is never consulted on that path, which is why people set it to 15 and see no change.

Now the timing. The backoff helper in 2.1.309 computes min(cap, base * 2^(attempt - 1)) and adds up to 25% proportional jitter. With the defaults (base 500 ms, cap 32000 ms) the waits after the first two 529s are about 0.5 s and 1 s. An overload that lasts ten seconds is enough to kill a run that had been working for an hour.

Sonnet and Haiku on an API key, and any model on a subscription login, do not hit the three-strike rule. They get the normal retry budget, but with the default delays that budget still covers only about 2.5 to 3.5 minutes:

RetryDefault delay (base 500, cap 32000)Tuned delay (base 4000, cap 64000)
10.5 s4 s
21 s8 s
32 s16 s
44 s32 s
58 s64 s
616 s64 s
7 to 1032 s each64 s each
Total for 10 retriesabout 160 s (+ up to 25%)about 444 s (+ up to 25%)

Minimal repro

You can’t make the API overload on demand, but you can reproduce the decision path. This is the shape of a job that fails during a capacity event:

# Claude Code 2.1.309, ANTHROPIC_API_KEY set, no subscription login
export CLAUDE_CODE_MAX_RETRIES=15   # looks like it should help; it doesn't for Opus 529s

claude --bare -p "Upgrade every package in this repo and fix the build" \
  --model opus \
  --permission-mode acceptEdits \
  --output-format json > result.json
echo "exit: $?"
jq '{is_error, api_error_status, result}' result.json

During an overload, result.json ends with is_error: true, api_error_status: 529, and the error string above, after three attempts. Everything the agent did before the failure is still in the session transcript, which matters for the recovery step at the end.

Fix 1: give the run a fallback model

This is the fix I reach for first, because it keeps the job moving instead of making it wait. Capacity is per model, so when Opus is overloaded, Sonnet usually is not.

# Claude Code 2.1.309
claude --bare -p "Upgrade every package in this repo and fix the build" \
  --model opus \
  --fallback-model sonnet,haiku \
  --permission-mode acceptEdits \
  --output-format json

Per the model configuration docs, --fallback-model takes a comma-separated chain, tried in order, capped at three models after duplicates are removed. To make it permanent for a repo or a machine, put it in settings:

{
  "model": "opus",
  "fallbackModel": ["sonnet", "haiku"]
}

Details that matter for long runs:

Setting a fallback also turns on the three-strike counter for every model, including Sonnet on a subscription. That is what you want: three quick 529s, then switch.

Fix 2: let an unattended run wait it out

If the job must run on one specific model (an eval harness comparing models, for example), switching is wrong. Use the retry watchdog, which the docs describe as meant “for unattended sessions such as eval harnesses, CI jobs, or remote workers”:

# Claude Code 2.1.309 (watchdog: 2.1.186+, MAX_WAIT: 2.1.295+)
export CLAUDE_CODE_RETRY_WATCHDOG=1
export CLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS=1800000   # give up after 30 min per request

claude --bare -p "Run the eval suite in evals/ and write report.md" \
  --model opus \
  --output-format json

With the watchdog on:

Always pair it with CLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS. Without it, the wait has no limit, and a CI job will sit there until the runner’s own timeout kills it, which is the worst outcome: you burn runner minutes and get no clean error. Thirty minutes per request is a sensible ceiling for a nightly job, five to ten for a PR check.

If you also set a fallback model, the fallback still wins: the bundle checks the three-strike fallback before the watchdog. So --fallback-model plus the watchdog means “switch after three 529s, then wait patiently on the fallback if it is overloaded too”, which is a good default for scheduled jobs.

Fix 3: spread the normal retries over a longer window

When you want neither a model switch nor an open-ended wait, keep the normal retry budget and stretch the 529 backoff. Two variables control it:

VariableDefaultAllowed rangeRequires
CLAUDE_CODE_OVERLOADED_RETRY_BASE_DELAY_MS500500 to 320002.1.292
CLAUDE_CODE_OVERLOADED_RETRY_MAX_DELAY_MS3200032000 to 640002.1.296
# Claude Code 2.1.309, Sonnet on an API key (no three-strike rule)
export CLAUDE_CODE_OVERLOADED_RETRY_BASE_DELAY_MS=4000
export CLAUDE_CODE_OVERLOADED_RETRY_MAX_DELAY_MS=64000
export CLAUDE_CODE_MAX_RETRIES=15

claude --bare -p "Triage the open issues labelled needs-triage" \
  --model sonnet \
  --output-format json

With base 4000 and cap 64000, ten retries span about 7.5 minutes and fifteen about 12.5 minutes. That rides out most short capacity dips without letting the job hang.

Three things trip people up here:

Make the settings stick in CI

Exporting variables in a shell is easy to lose. Put them in the job definition. In GitHub Actions:

# .github/workflows/nightly-agent.yml, Claude Code 2.1.309
jobs:
  agent:
    runs-on: ubuntu-latest
    timeout-minutes: 90
    env:
      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
      CLAUDE_CODE_RETRY_WATCHDOG: "1"
      CLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS: "1800000"
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g @anthropic-ai/claude-code@2.1.309
      - run: |
          claude --version
          claude --bare -p "$(cat .github/prompts/nightly.md)" \
            --model opus --fallback-model sonnet \
            --permission-mode acceptEdits \
            --output-format json > result.json

Keep timeout-minutes above CLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS plus the expected run time, or the runner kills the job before Claude Code can report a clean error. Pin the CLI version: the overload variables need 2.1.292 and 2.1.296, and an unpinned install on an old image gives you defaults without any warning. Print claude --version so the log shows which one ran.

Outside CI, the env block in .claude/settings.json (or ~/.claude/settings.json for a whole machine) does the same for every session, interactive or not:

{
  "env": {
    "CLAUDE_CODE_OVERLOADED_RETRY_BASE_DELAY_MS": "4000",
    "CLAUDE_CODE_OVERLOADED_RETRY_MAX_DELAY_MS": "64000"
  },
  "fallbackModel": ["sonnet"]
}

Resume instead of restarting when it still fails

Even with all of the above, a run can still end on a 529 when the max wait runs out. The work done so far is in the session transcript, so resume it rather than starting over. A small wrapper does this for scheduled jobs:

#!/usr/bin/env bash
# Claude Code 2.1.309, jq 1.7
set -uo pipefail
prompt="$1"
out=$(claude --bare -p "$prompt" --model opus --fallback-model sonnet --output-format json)

for attempt in 1 2 3; do
  status=$(jq -r '.api_error_status // empty' <<<"$out")
  [[ "$status" != "529" ]] && break
  session=$(jq -r '.session_id' <<<"$out")
  echo "529 on attempt $attempt, resuming $session in 5 minutes" >&2
  sleep 300
  out=$(claude --bare -p "continue" --resume "$session" \
        --model opus --fallback-model sonnet --output-format json)
done

jq -r '.result' <<<"$out"
jq -e '.is_error | not' <<<"$out" > /dev/null

The headless docs confirm that --resume accepts the session ID from the JSON output, from any directory on the same machine. In a CI runner that is a fresh VM per job, the transcript disappears with the VM, so this outer loop belongs inside one job, not across jobs. If you stream with --output-format stream-json --verbose, each retry also emits a system/api_retry event with attempt, max_retries, retry_delay_ms, error_status and error: "overloaded", which is the cleanest way to log how close a run came to failing. For how to consume that stream, see streaming nested subagent output from a headless run.

Gotchas that look like the same problem

Sources

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Back