プログラマティックツールコーリングは4分でタイムアウトし多くの場合リトライされる
プログラマティックツールコーリングの保留は約4分でTimeoutErrorになります。5分前後のコンテナのアイドル検知や90秒のセル制限との違い、公式ドキュメント記載の実際のエラー文言と対策を扱います。
このTipsでできること
プログラマティックツールコーリングでツール結果を返すのが遅れると、Claudeのコード側でTimeoutErrorが発生します。待ち時間の上限は約4分で、コンテナがアイドル状態のまま約5分たったときの扱いとは別の話です。
この記事では、保留から実際にタイムアウトするまでの挙動、公式ドキュメントに載っているエラー文言、名前の似た時間制限との見分け方を扱います。最後に、ツール側で先に打ち切るための実装例も付けます。
保留状態とは何か — コードがtool_resultを待って一時停止している
プログラマティックツールコーリングでは、Claudeがコード実行環境の中でツールを関数として呼び出します。ツール関数が呼ばれると、コード実行はそこで一時停止します。APIはstop_reason: "tool_use"とともにレスポンスを返します。
このtool_useブロックのcallerフィールドには、呼び出し元となったcode_executionブロックのidが入ります。
{
"type": "tool_use",
"id": "toolu_def456",
"name": "query_database",
"input": { "sql": "<sql>" },
"caller": {
"type": "code_execution_20260120",
"tool_id": "srvtoolu_abc123"
}
}ここで止まっているのは、新しいツール呼び出しの起動待ちではありません。すでに実行が始まっているPythonコードが、tool_resultを待って一時停止している状態です。tool_resultを送るとコードの実行が再開され、待っていた関数呼び出しに戻り値が渡ります。この一時停止と再開は、複数回の往復になることもあります。
保留から再開までの流れ
- 1
Claudeのコードがツール関数を呼ぶ
コード実行が一時停止し、
stop_reason: "tool_use"とcontainerのIDを持つレスポンスが返ります。 - 2
自分のアプリがツールを実行する
レスポンスの
callerとexpires_atを見て、DBクエリや外部APIなど実際の処理を走らせます。 - 3
tool_resultだけを載せて送り返す
containerのIDを付け、ユーザーメッセージにはtool_resultブロックだけを入れます。
保留から約4分でTimeoutErrorになる
公式ドキュメントによると、ツール結果が届かないまま約4分が経つと、Claudeの実行中のコード内でTimeoutErrorが発生します。実際のレスポンスは次の形です。
{
"type": "code_execution_tool_result",
"tool_use_id": "srvtoolu_abc123",
"content": {
"type": "code_execution_result",
"stdout": "",
"stderr": "TimeoutError: Calling tool ['query_database'] timed out (no response after 270s).",
"return_code": 0,
"content": []
}
}数字に注意が要ります。説明文は「約4分」、同じページのエラー例には270s(4分30秒)と出ています。「約」が付いた値なので、秒数は固定の仕様ではなく目安として読むのが無難です。
自前で余裕を取るなら、240秒でも270秒でもなく、それより十分手前に打ち切りの基準を置くのが安全です。具体的な値は後半の実装例で扱います。
もう1点、TimeoutErrorはreturn_code: 0のまま、stderrの文字列として届きます。HTTPエラーでもありません。エラー処理はreturn_codeだけで書かず、stderrにTimeoutErrorが含まれるかを見る必要があります。
タイムアウト後にClaudeは何をするか
公式ドキュメントは、TimeoutErrorの後の挙動を次のように説明しています。
Claude sees the error in
stderrand typically retries the call
ClaudeはstderrのTimeoutErrorを読み取り、多くの場合そのツール呼び出しを再試行します。"typically"(通常は)という書き方なので、必ず再試行するとは限りません。
再試行されるということは、遅いツールが同じ引数で2回走る可能性があるということです。副作用のあるツール(書き込み、送信、課金を伴う呼び出し)は、同じ呼び出しが重複しても害が出ないように作っておくと安心です。
「4分の待機」と「5分前後のコンテナ」と「90秒のセル」を切り分ける
時間に関する制限は、名前も数字も近いものが3つあります。どの層で起きるかが違うので、症状から見分けます。
コンテナ側の挙動は、コード実行ツールのページに詳しく書かれています。約5分の無操作でチェックポイント化され、作成から30日以内であれば、そのコンテナのIDを付けた要求で復元できます。一方、expires_atは30日の期限ではなく、より短い値で更新され続けます。作成から30日を過ぎたコンテナは復元できないので、containerを付けずに送り直します。
コード実行ツールの料金体系で触れた課金の最小単位(5分)とも別の数字です。課金の話と、タイムアウトの話は分けて覚えておくと混乱しません。
公式ドキュメントは、保留中のレスポンスにあるexpires_atより十分早く結果を返すよう書いています。自前でも経過時間を数えておくと安全です。
90秒のセル制限とexecution_time_exceededはどう違うか
コード実行ツールには、ツール呼び出し全体に共通するexecution_time_exceededというエラーコードもあります。1回のツール呼び出しが最大実行時間を超えると返り、error_codeフィールドに入ります。
これとは別に、プログラマティックツールコーリングでは、Pythonの1セルごとに90秒のウォールクロック制限があります。code_execution_20260120とcode_execution_20260521は同じランタイムです。違いは、後者がこの90秒の制限をツールの説明でClaudeに伝える点です。
セルが制限を超えても例外にはなりません。return_codeが非ゼロの通常のコード実行結果として返り、出力にdetection_timeoutというステータスメッセージが含まれます。
自分のツールが遅いのか、Claudeのコードのセルが重いのか、ツール呼び出し全体が長いのか。切り分けは、返ってきた形を見るのが早道です。
stderrにTimeoutError: Calling toolがあれば、ツール結果の待機切れ(約4分)です- 出力に
detection_timeoutがあり、return_codeが非ゼロなら、Pythonセルの90秒超過です error_codeがexecution_time_exceededなら、ツール呼び出し全体の最大実行時間を超えています
4分以内に返すレスポンスにも組み立ての決まりがある
保留中の呼び出しに答えるリクエストには、タイムアウトとは別に、守らないと拒否される決まりがあります。
1つ目は、containerのIDを必ず付けることです。保留中のプログラマティックツール呼び出しがある間、IDは省略できる項目ではなく必須で、付けないとAPIがリクエストを拒否します。通常のコード実行では、省略しても新しいコンテナが作られるだけです。
2つ目は、メッセージの中身です。保留中の呼び出しに答えるユーザーメッセージには、tool_resultブロックだけを入れます。テキストブロックは、tool_resultの後ろに置いても許されません。
この制限が当てはまるのは、プログラマティックな(コード実行からの)ツール呼び出しに答えるときだけです。通常のクライアントツールの結果なら、後ろにテキストを足せます。
tool_resultのcontentにも制限があります。文字列かtextブロックだけが許され、画像やドキュメントのブロックは拒否されます。スクリーンショットを返したいツールは、プログラマティック呼び出しの対象にしにくいことになります。
エラー処理は、2系統に分けておくと混ざりません。コード内のタイムアウトはstderrの文字列で見ます。リクエスト形式の不備は、APIの応答側で見ます。
実装側で対策する3点
公式ドキュメントがタイムアウト対策として挙げているのは、次の3点です。
expires_atフィールドを監視する: 一時停止したレスポンスに含まれる値で、残り時間を見る- ツール実行そのものにタイムアウトを実装する: 外部APIやDBクエリが長時間ハングしないよう、呼び出し側にも上限を置く
- 長い処理は小さな塊に分ける: 1回のツール呼び出しに時間のかかる処理を詰め込まない
サーバーツールとクライアントツールが混在するときの継続リクエストでも触れたとおり、継続リクエストではtools配列を維持し、containerのIDを正しく渡します。プログラマティックツールコーリングの全体像はAdvanced Tool Useにあります。
ツール側で先に打ち切る
遅くなりうるツールには、4分より十分短い自前の上限を設けます。ツール側で先に打ち切り、理由の分かるエラー文字列をtool_resultとして返せば、Claudeのコードはそれを見て別の処理へ分岐できます。
公式ドキュメントのエラー例も、"Error: Query timeout - table lock exceeded 30 seconds"のように、原因が読める文字列をtool_resultのcontentに入れる形です。次のコードは、Python(asyncio)でその考え方を書いた一例です。上限の秒数は自分の処理に合わせて決めます。
import asyncio
TOOL_LIMIT_SECONDS = 120 # 約4分より十分短い値の一例
async def run_tool(call):
try:
result = await asyncio.wait_for(
execute_query(call.input["sql"]), timeout=TOOL_LIMIT_SECONDS
)
return {"type": "tool_result", "tool_use_id": call.id,
"content": result}
except asyncio.TimeoutError:
return {"type": "tool_result", "tool_use_id": call.id,
"content": f"Error: query exceeded {TOOL_LIMIT_SECONDS}s. "
"Narrow the date range or add a LIMIT."}何も返さずに4分待たせると、Claudeに届くのはstderrのTimeoutErrorです。上のように理由と次の手を書いたエラー文字列を返せば、コード側で分岐する材料にできます。
長い処理を分ける
もう1つの手は、処理自体を分けることです。たとえば全期間の集計を1回で頼まれる場合、月ごとに複数回に分けて返す形にします。1回あたりの待ちが短くなり、TimeoutErrorにも、90秒のセル制限にも触れにくくなります。
周辺の制約も押さえる
タイムアウトの対策を組むときに、同じページの制約表と矛盾しないかも見ておきます。公式ドキュメントには、プログラマティック呼び出しでは使えないものが並んでいます。
strict: trueのツールは、プログラマティック呼び出しと併用できませんtool_choiceで、特定のツールをプログラマティックに呼ぶよう強制できませんdisable_parallel_tool_use: trueは、プログラマティック呼び出しでは使えません- MCPコネクタ経由のツールは、プログラマティックには呼べません
遅いツールを4分以内に収める工夫をしても、そもそもそのツールがプログラマティック呼び出しの対象外なら話が変わります。対象外のツールは、通常のツール呼び出しとして組む選択になります。
まとめ
保留中のtool_resultは約4分でTimeoutErrorになります。コンテナのアイドル(約5分)や90秒のセル制限とは別の時計なので、症状から見分けます。遅くなりうるツールは、4分より十分短い自前の上限で先に打ち切り、理由つきのエラーを返す形が扱いやすくなります。