Claude Media
Claude Codeのapi_retryをstream-jsonで拾いリトライを監視する

Claude Codeのapi_retryをstream-jsonで拾いリトライを監視する

claude -pのstream-jsonに流れるsystem/api_retryを拾い、リトライの進み具合と原因を監視するjqスクリプトを組みます。no_responseの読み方と、apiKeyHelperで2回無音になる条件も扱います。

claude -pを--output-format stream-jsonで動かすと、APIが一時的に失敗してClaude Codeが再試行するたびにsystem/api_retryというイベントが1行流れます。この行を拾えば、CIやラッパースクリプトの側で「いま何回目のリトライか」「何が原因か」「次まで何秒待つか」を、最終結果を待たずに把握できます。

この記事では、イベントのフィールドを監視の目的別に読み替え、jqで動くフィルタを作ります。no_responseが付く場合の読み方と、apiKeyHelperの認証失敗で最初の2回が静かに過ぎる条件も扱います。

api_retryが流れる条件と、流れない条件

イベントが出るのは、リトライ可能なエラーで失敗し、再試行に入る直前です。再試行に入らない失敗は、そもそも再試行のイベントを伴いません。

何が再試行の対象になるかは、エラーページの「Automatic retries」に一覧があります。イベントが流れうる側には、次のような失敗が並びます。

  • 応答が流れ始める前のサーバーエラー、過負荷、リクエストのタイムアウト
  • 応答の途中で切れた接続(思考が終わる前なら同じ要求を再送する)
  • 一時的な429の制限(ゲートウェイの利用上限による429は含まれない)
  • 要求のトークン数がコンテキスト上限を超えて拒否されたとき(max_tokensを減らして再送する)
  • ヘッダーが時間内に届かない要求(最大1回だけ再送)

逆に、再試行されない失敗はイベントを伴いません。

  • TLS証明書の検証失敗。最初の試行でそのままエラーになる(ハンドシェイクのタイムアウトのような一時的な条件は再試行される)
  • 組織のポリシー確認が拒否した要求。別のモデルにも再送されない
  • 応答の途中まで進んだ後の失敗。完成した部分を残して、ツール呼び出しの結果から続きを進める
  • 応答が完了した後の失敗。そのままターンが正常に終わる

監視では、イベントが出ない失敗が別にあることを前提にします。さらに、イベントが出るはずの場面でも欠ける条件が2つあります。

  • apiKeyHelperの認証情報が401/403で拒否される場合、v2.1.246以降は最初の2回のリトライでイベントを出さない。3回目の連続リトライから出る
  • 静かに過ぎた2回もattemptには数えられる

後者(静かな2回もattemptに数えられる点)が実務で効きます。apiKeyHelperを使う環境で最初に届くイベントがattempt: 3でも、1行目と2行目の欠落ではありません。「attemptは1から連番で届く」という前提で欠番を異常扱いするスクリプトは、この環境で誤報を出します。

このイベントは再試行の直前の知らせで、リトライが尽きたあとの検知は別の層の仕事です。OpenTelemetryで枯渇を見る方法はretry exhaustion(リトライ枯渇)をClaude Codeで検知する方法にまとめています。ストリーム側で終わりを判定するなら、末尾のresultメッセージ(最後の行に最終応答とコスト、セッション情報が入る)とプロセスの終了コードを見ます。

フィールドを監視の用途に読み替える

イベントのフィールドは次のとおりです。

フィールド型監視での使い道
attempt型整数(1始まり)監視での使い道進み具合。閾値アラートの軸
max_retries型整数監視での使い道この失敗の原因に許されるリトライの総数
retry_delay_ms型整数監視での使い道次の試行までの待ち時間
error_status型整数またはnull監視での使い道HTTPステータス。応答自体が無いときはnull
no_response型オブジェクト(任意)監視での使い道ヘッダーが時間内に届かなかったときだけ付く
error型文字列監視での使い道原因の分類
uuid / session_id型文字列監視での使い道重複排除とセッションの紐づけ

max_retriesの説明は「この失敗の原因に許されるリトライの総数」です。セッション全体の予算ではなく原因ごとの上限なので、attempt / max_retriesの比率は「いまの原因がどこまで進んだか」を表します。

errorの取りうる値は次の12種類です。

authentication_failed / oauth_org_not_allowed / account_on_hold / billing_error / rate_limit / overloaded / invalid_request / model_not_found / server_error / max_output_tokens / cloud_credential_error / unknown

監視では、この12種を「待てば直りうるもの」と「人が動かないと直らないもの」に分けると通知先を振り分けやすくなります。次の分類は、名前から私が仕分けた一例で、公式の区分ではありません。

振り分けerrorの例通知の考え方
待てば直りうるerrorの例overloaded / rate_limit / server_error通知の考え方閾値を超えたときだけ通知
設定・契約の問題errorの例authentication_failed / billing_error / account_on_hold / oauth_org_not_allowed通知の考え方初回から通知
リクエスト側の問題errorの例invalid_request / model_not_found / max_output_tokens通知の考え方再試行では直らないので呼び出し側を見る

分類名と中身の対応が曖昧なunknownは、error_statusとno_responseの有無で補って読みます。

対話画面のスピナーは、再試行中にRetrying in Ns · attempt x/yというカウントダウンを出します。ラベルは、すぐ対処できる失敗(ネットワーク断・TLSハンドシェイク失敗・レート制限)なら最初から理由を示し、それ以外は最初API errorと表示します。v2.1.198以降は3回目の試行から具体的な理由に切り替わります。スピナーの文言とストリームのerrorの値は別物なので、画面の表示を前提にした監視条件は組みません。

jqでリトライを1行に整形する

まず、イベントを人が読める1行にします。stream-jsonには--verboseが必要です。

claude -p "テストを実行して失敗を直して" \
  --output-format stream-json --verbose |
  jq -r 'select(.type == "system" and .subtype == "api_retry")
    | "retry \(.attempt)/\(.max_retries) | wait \(.retry_delay_ms/1000)s"
      + " | status \(.error_status // "none") | \(.error)"
      + (if .no_response
         then " | no_response waited \(.no_response.waited_ms/1000)s"
              + " retry_wait \(.no_response.retry_wait_ms/1000)s"
         else "" end)'

手元のjq 1.7.1で、公式のフィールド表に沿って作った3行のサンプルJSONL(overloaded、no_response付き、authentication_failed)にこのフィルタをかけたところ、次の出力になりました。サンプルは自作のもので、実際のAPI障害から採った出力ではありません。

retry 3/10 | wait 2s | status 529 | overloaded
retry 1/10 | wait 1s | status none | unknown | no_response waited 180s retry_wait 599s
retry 4/10 | wait 4s | status 401 | authentication_failed

error_statusがnullのときに// "none"で受けているのは、nullをそのまま文字列に埋めるとnullと表示されて読みにくいためです。resultメッセージなど、api_retry以外の行はselectで落ちるので何も出ません。

no_responseが付いたら待ち時間の内訳を読む

error_statusがnullでno_responseが付くイベントは、「HTTPエラーが返った」のではなく「応答ヘッダーが時間内に届かなかった」ことを示します。waited_msはその失敗した試行が待った時間、retry_wait_msは次の再試行が待つ時間です。この項目はv2.1.261以降で付きます。

エラーページの「No response from API」の節によると、最初の試行の待ち時間はCLAUDE_STREAM_FIRST_BYTE_TIMEOUT_MS(1以上を設定した場合は10秒〜30分に丸められる)で決まり、設定が無ければバイト単位の無通信監視の既定値が使われます。どちらにもリクエスト本文32KBごとに1秒が加わります。再試行側はAPI_TIMEOUT_MSの1秒手前で、既定なら10分弱です。

監視に引き直すと、こう読めます。

  • waited_msが長く、retry_wait_msがほぼAPI_TIMEOUT_MS: プロキシやゲートウェイが応答を完了まで保留している可能性を疑う
  • 何度もwaited_msだけで失敗し、再試行で成功する: 最初の試行の待ちが短すぎる。CLAUDE_STREAM_FIRST_BYTE_TIMEOUT_MSを上げる候補
  • retry_wait_msの値が毎回同じ: 設定値どおりに動いている

再試行も無応答で終わると、ターンは「API Error: No response from API」で終わり、メッセージに各試行の待ち時間が出ます。Bedrockでは再試行も最初と同じ期限になります。

閾値を超えたときだけ知らせる

全部のイベントを通知すると、混雑時間帯はノイズになります。attemptが上限に近づいたものだけを標準エラーへ出し、生ログは別に残す構成にします。

claude -p "$PROMPT" --output-format stream-json --verbose |
  tee run.jsonl |
  jq -r 'select(.type == "system" and .subtype == "api_retry"
                and .attempt >= (.max_retries - 2))
    | "WARN \(.error) attempt \(.attempt)/\(.max_retries)"' >&2

.max_retries - 2は「あと2回で尽きる」位置です。2という数は私が置いた例で、通知を早めたければ引く数を増やします。max_retriesは原因ごとに変わりうるので、固定値の8などと比べるより、イベント自身の値から引くほうが安全です。

teeで生ログを残しておくと、アラートが出たあとにerror_statusの並びやuuidで出来事を追えます。stream-json全体の読み方や、サブエージェントのメッセージの扱いはClaude Codeのstream-json出力を扱うで整理しています。

待ち方を変えると、イベントの見え方も変わる

リトライの回数と待ち時間は環境変数で変えられ、変えるとイベントの値に出ます。

環境変数既定イベントへの影響
CLAUDE_CODE_MAX_RETRIES既定10イベントへの影響max_retriesの目安。v2.1.186以降は上限15
CLAUDE_CODE_RETRY_WATCHDOG既定未設定イベントへの影響1で429と529を無期限に再試行する。attemptは大きくなり続ける
CLAUDE_CODE_OVERLOADED_RETRY_BASE_DELAY_MS既定500イベントへの影響529のretry_delay_msの出発点(v2.1.292以降)
API_TIMEOUT_MS既定600000イベントへの影響no_responseの再試行側の待ちに効く

表の挙動には、境界の条件がいくつかあります。

  • CLAUDE_CODE_MAX_RETRIESの上限15は、CLAUDE_CODE_RETRY_WATCHDOGを付けると外れる(v2.1.199以降)。ウォッチドッグ側はサーバーエラーや接続断などの既定の回数も300に引き上げ、バックオフは合計でおよそ3時間分になる
  • 429と529の待ちには、既定で上限がない。CLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS(v2.1.295以降)を設定すると、1回のAPI要求がこの2種のエラーを待つ時間を区切れる
  • 利用上限や使用クレジットの枯渇を報告する429は、標準速度の要求ならウォッチドッグを付けていても即座に失敗する(v2.1.239以降)
  • CLAUDE_CODE_OVERLOADED_RETRY_BASE_DELAY_MSは32000まで上げられる。ウォッチドッグが1のときと、高速モードで送った要求には効かない

CLAUDE_CODE_RETRY_WATCHDOGを付けたCIでは、attempt >= max_retries - 2のような閾値が意味を持たなくなります。無人の長時間ジョブで上限が無いなら、閾値はattemptの絶対値ではなく、同じerrorが続いた時間で決めるほうが合います。529の待ち方そのものはClaude Codeで529 Overloadedが続くときに詳しく書いています。

CLAUDE_CODE_MAX_RETRIESを下げると、失敗を早く表に出せます。スクリプト側で独自にやり直すなら、Claude Code側のリトライ回数を絞り、api_retryの通知を起点に呼び出し側の再実行へ渡す設計もあります。

よくあるつまずき

つまずき

api_retryの監視で起きやすい3つの失敗

  • --verboseを付け忘れる

    公式の例は--output-format stream-jsonに--verboseを付けた形です。付け忘れたときにイベントが流れるかはドキュメントに記載がないため、例どおりに揃えます。

  • 欠番を異常扱いする

    apiKeyHelperの401/403では最初の2回が無音です。attemptが3から始まっても欠落ではありません。

  • イベントの有無を成否と取り違える

    api_retryが流れたことは再試行が起きたことを示すだけです。成否は末尾のresultか終了コードで判定します。

よくある質問

api_retryが1行も出ない実行は、障害が無かったということですか

再試行に入った失敗が無かった、という意味です。リトライ対象外の失敗(TLS証明書の検証失敗など)は再試行されず、イベントも出ません。失敗の有無はresultメッセージと終了コードで確かめます。

認証が壊れているのに、イベントがなかなか来ません

apiKeyHelperが返した認証情報が401/403で拒否される場合、v2.1.246以降は最初の2回のリトライが無音です。3回目の連続リトライからauthentication_failedなどのイベントが出ます。

まとめ

system/api_retryは、リトライの途中経過を最終結果より先に取れるストリーム上のシグナルです。attemptとmax_retriesの差で進み具合を、errorで通知先の振り分けを、no_responseで待ち時間の設定の当たり外れを読めます。一方で、apiKeyHelperの無音の2回と、CLAUDE_CODE_RETRY_WATCHDOGによる上限の消失は、閾値を決めるときに前提が変わる点です。まずteeで生ログを残し、自分の環境で実際に流れるerrorとattemptの分布を見てから、アラートの位置を決める進め方が安全です。

この記事を共有:XはてブLinkedIn