Claude Media
Claude APIで長時間リクエストがタイムアウトする対策

Claude APIで長時間リクエストがタイムアウトする対策

Claude APIの非ストリーミングリクエストは10分の壁でSDKに事前ブロックされます。ストリーミングAPIとBatch APIの使い分け、TCP keep-aliveの設定まで実装方法を扱います。

症状で見分ける: ValueError・接続切れ・504は別の問題

「長いリクエストがタイムアウトする」と言っても、原因は3種類あります。どれに当たったかで、効く対策が変わります。

手順

エラーの出方から原因を切り分ける

  1. 1

    送信前に ValueError で止まる

    「Streaming is required for operations that may take longer than 10 minutes」というメッセージです。SDKが非ストリーミングの max_tokens から所要時間を見積もり、約10分を超えると判断して送信を止めています。ネットワークの問題ではなく、設定の問題です。

  2. 2

    応答が来ないまま接続が切れる

    経路上のロードバランサーやプロキシがアイドル接続を落とすケースです。リクエストを送ったあと、Anthropicからの応答を受け取らないまま接続が切れます。クライアントには接続エラーやタイムアウトとして見えます。サーバー側で処理が進んでいた可能性は残ります。

  3. 3

    504 timeout_error が返る

    リクエストはサーバーまで届き、処理中にタイムアウトしたことを示します。エラーの定義は「The request timed out while processing」です。

1つ目は手元で再現できます。2つ目と3つ目は環境や負荷に依存するため、対策の考え方が違います。以降は症状ごとに見ていきます。

ValueErrorが出るmax_tokensの境目を実機で試す

SDKの事前検証がどこで効くのかを、Python SDK 1.11.0で確かめました。接続先は 127.0.0.1:9 にします。何も待ち受けていないポートです。ダミーのAPIキーで max_tokens を変えながら messages.create を呼ぶと、検証に引っかかったかどうかだけが分かります。

import anthropic
 
c = anthropic.Anthropic(api_key="dummy", base_url="http://127.0.0.1:9", max_retries=0)
 
def blocked(n, **kw):
    try:
        c.messages.create(model="claude-sonnet-5", max_tokens=n,
                          messages=[{"role": "user", "content": "hi"}], **kw)
    except ValueError:
        return True   # SDKの事前検証で止まった
    except Exception:
        return False  # 検証は通り、接続の段階で失敗した
 
lo, hi = 21000, 21334
while hi - lo > 1:
    m = (lo + hi) // 2
    lo, hi = (lo, m) if blocked(m) else (m, hi)
print("最初に止まる max_tokens:", hi)
print("timeout=900 を付けた 128000:", blocked(128000, timeout=900))

出力は次のとおりです。

最初に止まる max_tokens: 21334
timeout=900 を付けた 128000: False

非ストリーミングでは、max_tokens が21,334になった時点でValueErrorが出ました。21,333までは検証を通ります。もう1つの結果は、timeout を明示すると128,000でも止まらないということです。Python SDKのドキュメントにも、stream=True かタイムアウトの上書きでこのエラーは無効になると書かれています。

この境目は、SDKの言語やバージョン、モデルによって動く可能性があります。ドキュメントの説明も「約10分」としか書いていません。数値を設計の前提にせず、自分の環境で同じ手順を試す使い方が安全です。

タイムアウトを延ばせばいい、とは言えない

検証を通すだけなら timeout を長くする手があります。ただし、Python SDKのドキュメントに別の注意があります。非ストリーミングで見込みの所要時間がタイムアウトより長いと、クライアントは応答を受け取らないまま接続を切り、リトライします。

つまり、timeoutの上書きは「エラーを消す」だけで、アイドル接続が経路で落とされる問題は残ります。

Python SDKの既定のタイムアウトは10分で、timeout オプションに秒数か httpx2.Timeout を渡して変えられます。クライアント全体の既定にも、リクエスト単位の with_options にも指定できます。

client.with_options(timeout=900.0).messages.create(...)  # このリクエストだけ15分

細かく分けたいときは、httpx2.Timeout(60.0, read=5.0, write=10.0, connect=2.0) のように接続・読み取り・書き込みを別々に指定できます。タイムアウトに達すると、SDKは APITimeoutError を投げます。

タイムアウトしたリクエストも、既定で2回リトライされます。延ばした分だけ、1回の失敗で待たされる時間が膨らむ点に注意が要ります。ストリーミングなら、この種の待ち時間をそもそも作りません。大きな max_tokens を使うなら、ストリーミングかBatch APIとあわせるのが前提です。選べる方式は2つあります。

ストリーミングAPIに切り替える

ストリーミングはレスポンスをイベント単位で受け取り続けるため、接続が無通信になりません。SDKの事前検証も、ストリーミング呼び出しには働きません。手元でも messages.stream は検証を素通りし、接続の段階(ここでは待ち受けのないポート)で失敗しました。

逐次表示が要らないなら、SDKにストリームを消費させて完成した Message を受け取れます。Pythonなら get_final_message()、TypeScriptなら finalMessage() です。非ストリーミングと同じ形の結果が返るので、既存コードの変更は小さく済みます。CLIでイベントを逐次出力する例は次のとおりです。

ant messages create --stream --format jsonl <<'YAML'
model: claude-sonnet-5
max_tokens: 128000
messages:
  - role: user
    content: Write a detailed analysis...
YAML

Batch APIに切り替えてポーリングする

接続を維持できない環境や、リアルタイム性が要らない処理にはMessage Batches APIが向きます。リクエストを送ったあとはサーバー側で非同期に処理され、結果をポーリングで取りに行く設計なので、途切れない接続を前提にしません。

数字

Batch APIの数字

  • 処理期限

    24時間

    多くは1時間以内に完了。超えた分は expired で課金なし

  • 1バッチの上限

    10万件 / 256MB

    先に届いた方が上限

  • 結果の保持

    29日

    processing の終了時刻ではなく作成時刻から数える

  • 料金

    標準の50%

    入力・出力トークンとも

いずれも公式ドキュメントの記載

ポーリングは processing_status が ended になるまで繰り返すだけです。

while :; do
  status=$(curl -s "https://api.anthropic.com/v1/messages/batches/$BATCH_ID" \
    -H "x-api-key: $ANTHROPIC_API_KEY" \
    -H "anthropic-version: 2023-06-01" \
    | jq -r '.processing_status')
  [ "$status" = "ended" ] && break
  sleep 30
done

止めたいときはキャンセル用のエンドポイントを使います。直後の processing_status は canceling で、同じポーリングで完了(ended)まで待てます。キャンセル済みのバッチには、それまでに処理が終わった分の結果が部分的に入ります。

終了後は、バッチ内のリクエストごとに結果が付きます。種類は4つで、succeeded 以外は課金されません。

  • errored: リクエストが不正だった、または内部エラーでメッセージが作られなかった
  • canceled: キャンセルが先に効き、モデルに送られなかった
  • expired: 24時間の期限までにモデルへ送られなかった

長い生成を投げたバッチでは、expired の再投入が現実的な後処理になります。バッチを同じ内容で作り直せば済み、期限切れ分の費用は発生しません。

結果は results_url のJSONLファイルから取ります。結果のファイルは大きくなりやすいため、一括ダウンロードよりストリームで読み出す実装が推奨されています。料金や制限の詳細はClaude Batch APIの使い方にあります。

出力が30万トークン級なら最初からBatch APIで組む

30万トークン級の生成は、1回で1時間を超えることがあります。この規模は、24時間の処理窓を前提に提出を計画する必要があります。output-300k-2026-03-24 ベータヘッダーをバッチに付けると、max_tokens の上限が通常の128kから30万に上がります。対象はClaude Opus 5.5・Opus 5・Opus 4.8・Opus 4.7・Opus 4.6と、Sonnet 5.5・Sonnet 5・Sonnet 4.6です。この拡張はバッチリクエスト向けで、同期のMessages APIには適用されません。使えるのはClaude APIとClaude Platform on AWSで、Amazon Bedrock、Google Cloud、Microsoft Foundryでは提供されていません。クラウド経由で呼んでいる場合は、この点を先に確かめてください。

料金は標準の50%のままで、プロンプトキャッシュの割引とも重ねられます。バッチは並行処理されるためキャッシュヒットは保証されず、ドキュメントでは、通常30%から98%の範囲とされています。

ストリーミングとBatch APIの選び分け

くらべる

どちらに寄せるか

画面にすぐ出したい

ストリーミングAPI

ユーザーの画面にリアルタイムで結果を出す処理や、単発の対話に向きます。イベントを受け取り続けるので接続が無通信になりません。標準料金のままで、50%割引はありません。

まとめて・安く・待てる

Batch API

数百〜数万件の一括処理や、社内プロキシ経由など接続が不安定な環境に向きます。接続を張り続けずに済み、料金は半額です。結果は24時間以内に届く前提で、即時性は求められません。

両方を同時に使うことはできません。バッチは stream: true に対応しておらず、結果は1つのファイルでまとめて返ります。

大量処理の進み具合を利用者に見せたいなら、request_counts の件数をポーリングのたびに画面へ流す方法があります。processing_status と並んで、処理中・成功・エラー・キャンセル・expiredの件数がバッチのレスポンスに入っています。個々の完了を利用者へ見せたいなら、ポーリングで結果を受け取った時点でアプリ側から通知する形になります。

504はSDKが自動リトライする

検証をすり抜けたあとのネットワーク越しの timeout_error(504)は、接続エラーや5xxと同じ扱いです。SDKが指数バックオフで、既定では2回までリトライします。retry-after ヘッダーがあればそれに従い、回数はクライアントの max_retries(TypeScriptなどは maxRetries)で変えられます。

自前のリトライを重ねると、SDKの分と合わせて二重に走ります。リトライの挙動を変えたいときは、まずこのオプションから調整します。

リトライの対象は、接続エラー、408、409、429、500以上のエラーです。max_retries=0 なら無効になり、リクエスト単位では with_options(max_retries=5) のように変えられます。

原因を調べる場面では、レスポンスの request-id ヘッダーが手がかりになります。エラー本文にも同じ値が request_id として入ります。PythonとTypeScriptのSDKは、レスポンスオブジェクトの _request_id で読めます。サポートへ問い合わせるときは、この値を添えます。

ストリーミング中のエラーは200の中に届く

ストリーミングにしても、エラーが起きなくなるわけではありません。サーバー送信イベント(SSE)では、いったんHTTP 200を返したあとにエラーイベントが流れてくることがあり、ステータスコードの判定では拾えません。タイムアウトや切断で終わる場合と、エラーイベントとして通知される場合の両方を処理に入れます。

200の中に隠れた異常の具体例は、次の記事にあります。Claude APIのrefusal stop_reasonを検出してリセットする方法です。

自前のHTTPクライアントではTCP keep-aliveを設定する

SDKを使わず直接APIにつなぐ場合は、TCPソケットのkeep-aliveを有効にすると、一部のネットワークでアイドル接続タイムアウトの影響を減らせます。SDKはこのソケットオプションを自分で設定していて、上書きしたいときはカスタムの http_client を渡します。

Pythonで生のソケットに設定するなら、SO_KEEPALIVE を有効にします。

import socket
 
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)

NATやロードバランサーは、一定時間データが流れない接続を管理テーブルから消すことがあります。keep-aliveで定期的にパケットを流せば、その無通信の時間を作りにくくなります。ただし効果が出るのは一部のネットワークに限られ、10分の見積もりを超えた非ストリーミング呼び出しを通すものではありません。恒久策はストリーミングかBatch APIです。

まとめ

ValueErrorが出たら、max_tokens を削るかtimeoutを延ばす前に、リアルタイム性が要るのかを決めます。要るならストリーミング、要らないならBatch APIです。504や接続エラーはSDKのリトライが先に働くので、自前の再試行は最後に足します。個別のエラーコードの扱いはClaude APIのエラーハンドリング設計で扱っています。

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