LLM APIのタイムアウトは何秒にする?再試行してよいエラー・悪いエラー

LLM APIのタイムアウトは何秒にする?再試行してよいエラー・悪いエラー AI開発

OpenAI APIやClaude API、Gemini APIをWebサービスへ組み込むと、タイムアウトを何秒に設定すればよいのか迷うことがあります。

10秒では長い回答が途中で失敗しやすく、10分ではユーザーが離脱したあともサーバーがAPI応答を待ち続ける可能性があります。

さらに注意したいのが再試行です。

タイムアウトしたリクエストを何度も再試行すると、ユーザーの待ち時間とAPI料金が増えます。一方、500や503などの一時障害を一度も再試行しない設計では、本来なら数秒後に成功できたリクエストまでエラーになります。

結論として、すべてのLLM APIに共通する「30秒が正解」「60秒なら安全」という値はありません。

通常のチャットなら30〜60秒、短いJSON生成や分類なら15〜30秒程度を最初の設定として検証し、長文・Reasoning・大量コンテキストでは90〜300秒程度またはストリーミングへ切り替える方法が現実的です。数分以上掛かることが正常な処理は、同期HTTPリクエストのタイムアウトを延ばし続けるのではなく、BackgroundやBatchへ移行します。

これらの秒数は各社の公式固定値ではなく、Webアプリケーション向けの初期設定です。実際には自分のサービスでp95、p99の応答時間を測定して調整します。

この記事では、LLM APIのタイムアウト設計と、再試行してよいエラー・悪いエラーをTypeScriptの実装例とともに解説します。

スポンサーリンク
  1. SDKのデフォルト10分をそのまま使わない
  2. タイムアウトは一つではなく複数ある
  3. 通常チャットは30〜60秒から始める
  4. 短い分類・JSON生成なら15〜30秒でもよい
  5. 長文生成はタイムアウトを延ばすよりストリーミングする
  6. ストリーミングでは「全体時間」だけを見ない
  7. 数分以上が正常ならBackgroundやBatchへ移す
  8. OpenAI SDKではtimeoutとmaxRetriesを明示する
  9. Claude SDKでも同じ問題がある
  10. Node.jsではAbortSignal.timeoutを使える
  11. ユーザーキャンセルとタイムアウトをまとめる
  12. タイムアウトしたら1回は再試行できる
  13. 408は再試行できる
  14. 429はエラーコードまで確認する
  15. 500は再試行できる
  16. 503と529も再試行対象
  17. 504は一度再試行し、設計も確認する
  18. 400は自動再試行しない
  19. 401は再試行しない
  20. 403も再試行しない
  21. 404も基本的には再試行しない
  22. 409は内容を見て判断する
  23. 413は入力を小さくしてから再送する
  24. 429に指数バックオフを使う
  25. 最大再試行回数は1〜2回程度から始める
  26. リクエスト全体の時間予算を作る
  27. 再試行可能なエラーをコード化する
  28. 422も無条件では再試行しない
  29. ユーザーキャンセルは再試行しない
  30. ストリーミング途中のエラーを最初から再試行しない
  31. Function Callingではタイムアウト再試行に注意する
  32. タイムアウトを短くして再試行を増やすのも危険
  33. p95とp99を保存する
  34. モデル別にもタイムアウトを分ける
  35. リトライ後の合計料金も保存する
  36. タイムアウト時に別モデルへ切り替える
  37. 再試行回数より成功率を見る
  38. タイムアウト値の初期設定
  39. タイムアウトを決める順番
  40. LLM APIのタイムアウトに関するよくある質問
  41. まとめ

SDKのデフォルト10分をそのまま使わない

OpenAIの公式JavaScript SDKは、2026年8月時点で1回のリクエストタイムアウトが標準10分です。

さらに、接続エラー、408、409、429、500番台について標準で2回再試行します。つまり、タイムアウトを変更せずSDKの自動再試行も使っていると、アプリケーションが想定するよりかなり長時間待つ可能性があります。

Claudeの公式TypeScript SDKも、標準タイムアウトは10分、maxRetriesは2です。AnthropicのSDKソースにも、タイムアウトしたリクエストは再試行されるため、最悪の場合は指定したタイムアウトよりはるかに長くPromiseが完了しない可能性があると明記されています。

10分というSDK標準値は、「Web画面でユーザーが10分待つべき」という意味ではありません。

SDKは長い生成処理にも対応できる汎用ライブラリなので、Webアプリケーション側では用途に合わせて、より短いタイムアウトを設定します。

タイムアウトは一つではなく複数ある

LLM APIのタイムアウトを考えるときに、単純な「APIタイムアウト」だけで考えると原因を切り分けにくくなります。

実際には、接続開始までの時間、最初の応答が届くまでの時間、ストリーム中に次のデータが来るまでの時間、生成全体の時間、Webリクエスト全体の時間を分けて考えます。

たとえばユーザーがAIチャットへ質問したケースでは、次のような時間が発生します。

処理の内訳
ユーザー送信
↓
自社 APIへ到達
↓
認証・DB処理
↓
LLM APIへ接続
↓
最初のトークン
↓
生成中
↓
最後のトークン
↓
DB保存
↓
ユーザーへ完了通知

LLM APIだけを60秒にしても、自社APIの最大実行時間が60秒なら、最後のDB保存を行う前にWebリクエストそのものが切断される可能性があります。

そのため、LLMへ割り当てる時間は、Webリクエスト全体の制限より短く設定します。

通常チャットは30〜60秒から始める

短い質問へ数百〜数千トークン程度で回答する通常チャットでは、全体タイムアウトを30〜60秒程度から検証すると扱いやすくなります。

たとえば自社サービスとして60秒以内にユーザーへ成功または失敗を返したい場合、LLMに60秒すべてを渡してはいけません。

時間の分配例
Webリクエスト全体: 60秒

LLM: 45秒

DB・ログ・レスポンス整形: 15秒

というように余白を残します。

実際には固定値ではなく、正常リクエストのp95またはp99を記録します。

通常の応答がp95で8秒なのにタイムアウトを300秒へ設定している場合、障害発生時に必要以上に長く接続を保持することになります。

反対にp95が35秒なのにタイムアウトを30秒へ設定すれば、正常なリクエストまで大量に中断します。

短い分類・JSON生成なら15〜30秒でもよい

感情分類、カテゴリ分類、短いStructured Outputsなど、出力量が小さい処理では通常チャットより短いタイムアウトを設定できます。

たとえば分類APIなら20秒程度を初期値にして、タイムアウト時に1回だけ再試行する構成が考えられます。

分類APIの例
1回目: 最大20秒

一時エラー
↓
1~2秒待機

2回目: 残り時間内だけ実行

ただし、1回目20秒、再試行20秒という固定値ではなく、リクエスト全体に時間予算を設定するほうが安全です。

長文生成はタイムアウトを延ばすよりストリーミングする

数千トークン以上の長文を生成する場合、モデルが全文章を生成し終わるまで通常HTTPレスポンスを待つと、ユーザーから見ると何も起きていないように見えます。

OpenAIも、長い出力では通常レスポンスを待つのに時間が掛かるため、生成中の内容を順次受け取るStreaming Responsesを提供しています。

Claudeも長時間リクエストについてStreaming Messages APIの利用を案内しています。Claude APIで504 timeout_errorが発生する場合にも、長時間処理ではストリーミングを検討するよう公式ドキュメントに記載されています。

ストリーミング接続がネットワーク側の都合で途中で切れるケースについては、OpenAI APIのストリーミングが途中で切れる原因で別途解説しています。

通常レスポンスを180秒待つより、

ストリーミングの設計
30秒以内に最初のイベント
↓
その後はイベントを継続受信
↓
完了まで最大180秒

という設計にすると、ユーザー体験と接続監視を分離できます。

ストリーミングでは「全体時間」だけを見ない

ストリーミングでは、最初のデータが来るまでの時間と、途中でデータが止まった時間を別々に監視します。

たとえば全体タイムアウトを180秒にしていても、

異常の例
最初の20秒: 正常に文章を受信

その後120秒: イベントが一つも来ない

という状態は、正常な長文生成とは限りません。

実務では、最初のイベントを待つタイムアウトと、最後のイベントから次のイベントまでのアイドルタイムアウトを分ける方法があります。

監視項目の分離
初回応答: 30~60秒

ストリームのアイドル: 30~60秒

生成全体: 120~300秒

これも固定の公式推奨値ではなく、運用を始めるための設定例です。

Reasoningモデルでは最初の出力まで長く掛かる場合があるため、通常チャットと同じ初回30秒を無条件に適用しないようにします。

数分以上が正常ならBackgroundやBatchへ移す

複雑なReasoning、Deep Research、大量のツール呼び出しなどは、処理に数分以上掛かることがあります。

OpenAIはDeep Researchについて、処理が数十分掛かる場合があるため、Background modeの利用を推奨しています。Background modeは長時間処理をHTTP接続のタイムアウトや接続断から切り離すための機能です。

Claudeでも、非常に大きなThinking処理では長時間接続によるネットワーク問題を避けるためBatch Processingが案内されています。処理をBatchへ切り替える際の設計はBatch APIはいつ使うべき?通常APIとの違い・向いている処理を比較で詳しく解説しています。

したがって、

不十分な対策
5分掘かるから
timeout = 600000

とするだけでは根本的な対策になりません。

処理時間そのものが長いことが正常なら、

非同期化への変更
リクエスト受付
↓
ジョブID返却
↓
非同期処理
↓
完了
↓
結果取得

という構造へ変更します。

OpenAI SDKではtimeoutとmaxRetriesを明示する

Webアプリケーションでは、SDK標準の10分と2回再試行をそのまま使わず、アプリケーション側の時間予算に合わせます。

src/openai-client.ts
import OpenAI from "openai";

const openai = new OpenAI({
  apiKey:
    process.env.OPENAI_API_KEY,

  timeout: 30_000,

  maxRetries: 0,
});

OpenAIのJavaScript SDKではtimeoutをミリ秒で設定できます。タイムアウト時はAPIConnectionTimeoutErrorが発生します。SDK標準ではタイムアウトも自動再試行の対象です。

独自の再試行ロジックを実装する場合は、SDK側をmaxRetries: 0にして一元管理すると、実際に何回APIへ送信されたか把握しやすくなります。

Claude SDKでも同じ問題がある

ClaudeのTypeScript SDKも標準タイムアウト10分、標準再試行2回です。

独自制御するなら次のように設定できます。

src/anthropic-client.ts
import Anthropic from "@anthropic-ai/sdk";

const anthropic =
  new Anthropic({
    apiKey:
      process.env
        .ANTHROPIC_API_KEY,

    timeout: 30_000,

    maxRetries: 0,
  });

Claudeの公式SDKは、接続エラー、Rate Limit、5xxなどの一時障害を指数バックオフで標準2回再試行し、retry-afterがあればそれも考慮します。

アプリケーション側でもさらに2回再試行すると、想定よりAPI呼び出し数と待ち時間が増えるため注意が必要です。

Node.jsではAbortSignal.timeoutを使える

SDK固有のタイムアウト以外に、アプリケーション全体のデッドラインを管理したい場合はAbortSignalを利用できます。

Node.jsには、指定時間後に自動的にabortされるAbortSignal.timeout()があります。

src/fetch-with-abort.ts
const signal =
  AbortSignal.timeout(
    30_000,
  );

const response =
  await fetch(
    "https://example.com/api",
    {
      method: "POST",
      signal,
    },
  );

単純なPromise.race()だけでタイムアウトを実装するより、実際のHTTPリクエストへAbortSignalを渡すほうが安全です。

次のコードでは30秒後にPromise側だけがrejectしても、内部の処理そのものが停止するとは限りません。

危険な例
await Promise.race([
  callLlmApi(),
  new Promise(
    (_, reject) => {
      setTimeout(() => {
        reject(
          new Error(
            "timeout",
          ),
        );
      }, 30_000);
    },
  ),
]);

可能であればHTTPクライアントやSDKへAbortSignalを渡し、実際のリクエストも中断します。

ユーザーキャンセルとタイムアウトをまとめる

Node.jsではAbortSignal.any()を使い、複数のAbortSignalをまとめることもできます。

ユーザーが「生成停止」を押した場合と、サーバー側のタイムアウトを同じリクエストへ適用できます。

src/combine-abort-signals.ts
const userController =
  new AbortController();

const timeoutSignal =
  AbortSignal.timeout(
    45_000,
  );

const signal =
  AbortSignal.any([
    userController.signal,
    timeoutSignal,
  ]);

ユーザーが途中でキャンセルした処理を「一時的なタイムアウト」と誤認して再試行してはいけません。

キャンセルと障害を明確に区別します。

タイムアウトしたら1回は再試行できる

ネットワーク混雑や一時的なサービス負荷によるタイムアウトであれば、少し待って再試行すると成功する場合があります。

OpenAIもAPITimeoutErrorについて、数秒待ってから再試行する方法を案内しています。

ただし、同じ巨大な入力が毎回120秒でタイムアウトしている場合に、3回、4回と再送しても解決しない可能性が高くなります。

最初のタイムアウトでは一度だけ再試行し、繰り返す場合は、

確認項目
入力が大きすぎないか

max_output_tokensが大きすぎないか

Reasoningが重すぎないか

ストリーミングへ変更できないか

BackgroundやBatchへ移せないか

を確認します。

タイムアウトを「何度でも再試行できるエラー」として扱わないことが重要です。

408は再試行できる

HTTP 408 Request Timeoutは、一時的な通信問題として再試行できる代表的なエラーです。

OpenAIの公式JavaScript SDKも408を自動再試行対象にしています。

Geminiの公式トラブルシューティングでも、408、429、5xxなどの一時エラーだけを再試行するよう案内されています。

指数バックオフを使い、すぐ同じリクエストを連打しないようにします。

429はエラーコードまで確認する

429は「再試行すればよい」と覚えると危険です。

一時的なRPMやTPM超過なら、待って再試行できます。

しかし、利用枠そのものを使い切った429は数秒待っても直りません。

OpenAIでは429でもorganization_spend_limit_exceededproject_spend_limit_exceededorganization_usage_limit_exceededなどがあり、これらは料金や利用上限を変更するまで再試行しても復旧しないと明記されています。

Geminiでも429をrate_limit_exceededquota_exceededに区別しています。

rate_limit_exceededなら指数バックオフで再試行できますが、quota_exceededは日次Quotaのリセットを待つか上限を変更する必要があります。

したがって、

不十分な判定
if (status === 429) {
  retry();
}

という実装では不十分です。

エラーコードとRetry-Afterを確認します。429の再試行実装そのものはOpenAI APIの429エラーを直す方法で解説した指数バックオフと同じ考え方です。

500は再試行できる

HTTP 500は、プロバイダー側の一時障害である可能性が高いため、再試行対象にできます。

OpenAIは500系のInternal Server Errorを公式SDKの再試行対象にしています。

Claudeも500 api_errorについて指数バックオフでの再試行を案内しています。

Geminiも500 api_errorについて再試行するよう案内しています。

一度または二度の再試行で復旧しない場合は、別モデルや別プロバイダーへのフォールバックを検討します。

503と529も再試行対象

503 Service Unavailableは、サービスが一時的に過負荷または利用不能になっている状態です。

Geminiでは503 service_unavailableについて、待って指数バックオフで再試行するよう案内されています。

OpenAIでも503のサーバー高負荷について、短く待って再試行する方法が案内されています。

Claudeでは独自に529 overloaded_errorがあり、API全体の高負荷時に返されます。Claude公式SDKではこのような5xx系一時障害も再試行対象です。

これらはフォールバックとも相性がよいエラーです。

OpenAIが継続して503ならClaude、Claudeが529ならGeminiというように、OpenAI・Claude・Gemini APIを自動で切り替えるフォールバック設計へ移行できます。

504は一度再試行し、設計も確認する

504 Gateway Timeoutは一時的な障害の場合もあります。

しかし、毎回同じ長いリクエストだけ504になる場合は、単純な再試行ではなく処理方式を変更します。

Claudeでは504 timeout_errorについて、長時間リクエストならStreaming Messages APIを利用することが案内されています。

Geminiでも504 deadline_exceededはリクエストが期限内に完了しなかった状態で、クライアント側のDeadline設定を確認するよう説明されています。

1回目の504は再試行し、同じ入力で繰り返す場合は、

変更先の候補
ストリーミング

出力トークン削減

Thinking量削減

入力分割

Background

Batch

へ変更するほうが効果的です。

400は自動再試行しない

HTTP 400 Bad Requestは、入力内容やAPIパラメータに問題がある状態です。

Claudeでは400 invalid_request_errorがリクエスト形式や内容の問題を示します。

Geminiも400系では、未知のパラメータや不正な入力を修正するよう案内しています。Geminiの再試行ガイドでも、400や403のクライアントエラーは自動再試行しないよう明記されています。

同じJSONを3回送っても、3回同じエラーになるだけです。

コード、スキーマ、モデル名、パラメータを修正してから再送します。

401は再試行しない

401 UnauthorizedはAPIキーや認証情報の問題です。

OpenAIでは無効、期限切れ、失効したAPIキーなどがAuthentication Errorになります。

Claudeでも401はAPIキーや認証情報の問題です。

Geminiも401 authenticationではAPIキーを確認するよう案内しています。

数秒待ってもAPIキーは直らないため、自動再試行してはいけません。

運用アラートを出し、設定を修正します。

403も再試行しない

403 Forbiddenは権限不足です。

使用できないモデル、プロジェクト、機能などをリクエストしている場合があります。

Geminiの公式ガイドでも403は再試行しないクライアントエラーの例として挙げられています。

別のAPIキーや権限設定が必要であり、バックオフしても解決しません。

404も基本的には再試行しない

404はモデル名、ファイルID、リソースIDなどが存在しない場合に発生します。

Geminiではmodel_not_foundも404として定義されています。別モデルへフォールバックすることはできますが、同じモデル名を何度も再試行する必要はありません。

モデル廃止や設定ミスを障害として隠さないよう、404が発生した場合はログとアラートを残します。

409は内容を見て判断する

409 Conflictは、プロバイダーによって扱いが異なるため注意が必要です。

OpenAIの公式JavaScript SDKは409を自動再試行対象にしています。

Claudeでは、リソースが同時更新されたなどのConflictを解消してから再試行するよう案内しています。

Geminiでも409 abortedは上位のアプリケーション層で再試行できますが、already_existsの場合は既存リソースを確認する必要があります。

したがって409についても、

不十分な判定
status === 409

だけで無条件再試行せず、エラーコードを確認します。

413は入力を小さくしてから再送する

413 Payload Too Largeは、入力サイズがAPI上限を超えています。

ClaudeのMessages APIでは32MBなど、Endpointごとにリクエストサイズ制限があり、超えると413 request_too_largeが返されます。

同じ入力をそのまま再試行しても成功しません。

PDFを分割する、不要な会話履歴を削除する、画像を減らす、RAGで必要なチャンクだけを取得するといった対策が必要です。

429に指数バックオフを使う

再試行するときは、失敗直後に同じリクエストを送り直さないようにします。

Geminiは、一時障害について1秒、2秒、4秒、8秒のように待機時間を増やし、さらにランダムなJitterを加えることを推奨しています。

TypeScriptでは次のように実装できます。

src/retry-delay.ts
function sleep(
  milliseconds: number,
): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(
      resolve,
      milliseconds,
    );
  });
}

function getRetryDelay(
  retryNumber: number,
): number {
  const base =
    Math.min(
      8_000,
      1_000 *
        2 ** retryNumber,
    );

  const jitter =
    Math.floor(
      Math.random() * 500,
    );

  return base + jitter;
}

1回目なら約1秒、2回目なら約2秒、3回目なら約4秒となります。

ただしRetry-Afterヘッダーが返っている場合は、独自計算よりその値を優先します。

OpenAIとClaudeの公式SDKも、対象エラーではRetry-Afterを考慮して再試行します。

最大再試行回数は1〜2回程度から始める

リアルタイムなWebサービスでは、再試行を増やしすぎるとユーザーの待ち時間が急激に増えます。

1回30秒のタイムアウトを3回再試行すれば、バックオフを含めて2分近く待つ可能性があります。

そのため対話型処理では、まず1回、多くても2回程度を上限として検証するのが扱いやすくなります。

一方、ユーザーが待っていないバックグラウンドジョブなら、より長いバックオフと再試行回数を設定できます。

重要なのは、「API単位の最大回数」だけでなく「ユーザー操作全体の最大時間」を設定することです。

リクエスト全体の時間予算を作る

次の実装では、各API呼び出しを30秒に固定していません。

全体の時間予算から残り時間を計算します。

src/run-with-budget.ts
const TOTAL_BUDGET_MS =
  45_000;

const MAX_ATTEMPTS =
  2;

async function runWithBudget<T>(
  operation: (
    signal: AbortSignal,
  ) => Promise<T>,
): Promise<T> {
  const startedAt =
    Date.now();

  let lastError:
    unknown = null;

  for (
    let attempt = 0;
    attempt < MAX_ATTEMPTS;
    attempt += 1
  ) {
    const elapsed =
      Date.now() -
      startedAt;

    const remaining =
      TOTAL_BUDGET_MS -
      elapsed;

    if (remaining <= 0) {
      throw new Error(
        "LLM APIの全体タイムアウトに達しました。",
      );
    }

    const attemptTimeout =
      Math.min(
        25_000,
        remaining,
      );

    try {
      return await operation(
        AbortSignal.timeout(
          attemptTimeout,
        ),
      );
    } catch (error) {
      lastError = error;

      if (
        !isRetryableError(
          error,
        )
      ) {
        throw error;
      }

      if (
        attempt ===
        MAX_ATTEMPTS - 1
      ) {
        break;
      }

      const delay =
        Math.min(
          getRetryDelay(
            attempt,
          ),
          Math.max(
            0,
            TOTAL_BUDGET_MS -
              (
                Date.now() -
                startedAt
              ),
          ),
        );

      if (delay <= 0) {
        break;
      }

      await sleep(delay);
    }
  }

  throw lastError;
}

この方法なら、1回目で25秒使ったあとに、2回目でもさらに25秒を無条件に使うことを防げます。

再試行可能なエラーをコード化する

HTTPステータスだけでなく、エラーコードも確認します。

src/classify-http-error.ts
type RetryDecision =
  | "retry"
  | "do_not_retry"
  | "retry_after_fix";

function classifyHttpError(
  status: number,
  code?: string,
): RetryDecision {
  if (
    status === 408 ||
    status >= 500
  ) {
    return "retry";
  }

  if (status === 429) {
    if (
      code ===
        "organization_spend_limit_exceeded" ||
      code ===
        "project_spend_limit_exceeded" ||
      code ===
        "organization_usage_limit_exceeded" ||
      code ===
        "quota_exceeded"
    ) {
      return "retry_after_fix";
    }

    return "retry";
  }

  if (status === 409) {
    if (
      code ===
      "aborted"
    ) {
      return "retry";
    }

    return "retry_after_fix";
  }

  if (
    status === 400 ||
    status === 401 ||
    status === 403 ||
    status === 404 ||
    status === 413 ||
    status === 422
  ) {
    return "retry_after_fix";
  }

  return "do_not_retry";
}

これは複数プロバイダーをまとめて扱うアプリケーション向けの一例です。

実際にはOpenAI、Claude、Geminiのアダプターでエラーを共通形式へ変換してから判定すると管理しやすくなります。

422も無条件では再試行しない

422 Unprocessable Entityは、JSON自体は読めてもリクエスト内容を処理できないケースです。

OpenAIのJavaScript SDKでも422は標準の自動再試行対象には含まれていません。標準再試行対象は接続エラー、408、409、429、500番台です。

そのため、422が出たらエラー本文を確認し、入力やパラメータを修正してから再送する設計が安全です。

ユーザーキャンセルは再試行しない

ユーザーが生成停止ボタンを押した場合は、障害ではありません。

Geminiでも499 cancelledはクライアントがリクエストを中断した状態として扱われ、「No action needed」とされています。

ユーザーキャンセルをネットワークエラーと同じcatchへ入れてしまうと、

誤った挙動
ユーザー「停止」

↓

サーバー「一時障害だ。再試行しよう」

となってしまいます。

AbortErrorやキャンセル用のエラーを最初に判定します。

src/is-cancelled.ts
function isCancelled(
  error: unknown,
): boolean {
  return (
    error instanceof DOMException &&
    error.name ===
      "AbortError"
  );
}

ユーザーキャンセルは即座に終了します。

ストリーミング途中のエラーを最初から再試行しない

Streaming APIでは、HTTP 200が返ったあとにエラーイベントが発生する場合があります。

Claudeも、SSEストリーミングでは200レスポンス後にエラーが発生する可能性があり、通常のHTTPエラー処理とは別に扱う必要があると説明しています。

たとえばユーザーへすでに次の文章を表示したとします。

表示済みの文章
Node.jsのイベントループは、
非同期処理を効率的に...

ここでストリームが切れ、別リクエストを最初から実行すると、

新しい回答
Node.jsはJavaScriptを...

という新しい回答が生成されます。

そのまま画面へ追加すると文章が壊れます。

最初のトークンをユーザーへ送る前なら自動再試行できます。

一度でも出力済みなら、現在の出力を破棄して「再生成」とするか、生成失敗として終了するほうが安全です。

Function Callingではタイムアウト再試行に注意する

Function Callingを利用している場合、モデルAPIのタイムアウトとツール実行のタイムアウトを分離します。

タイムアウトの分離
モデル呼び出し: 30秒

DB検索ツール: 5秒

外部API: 10秒

というように、ツールごとに個別の制限を設定します。

特に注文、メール送信、決済、ファイル削除などの副作用を持つツールは注意が必要です。

ツール実行自体は成功したのに、その後のモデル呼び出しだけがタイムアウトした場合、ワークフロー全体を最初から再試行すると、同じツールを二重実行する可能性があります。

副作用を持つ処理ではOperation IDやIdempotency Keyを保存し、処理済みなら前回結果を返します。

タイムアウトを短くして再試行を増やすのも危険

「10秒で切って3回試せば速いサーバーに当たる」という設計もおすすめできません。

LLMでは入力の長さ、Thinking、出力量によって正常な応答時間自体が変化します。

正常なのに12秒掛かるリクエストを10秒で切れば、

失敗するパターン
1回目: 10秒で中断

2回目: 10秒で中断

3回目: 10秒で中断

となり、30秒待って一度も回答できない可能性があります。

タイムアウト値は推測ではなく実測値から決定します。

p95とp99を保存する

リクエストごとに処理時間をDBや監視サービスへ保存します。

src/llm-request-log.ts
type LlmRequestLog = {
  provider:
    | "openai"
    | "anthropic"
    | "gemini";

  model: string;

  operation: string;

  attempt: number;

  latencyMs: number;

  success: boolean;

  timeout: boolean;

  errorCode:
    string | null;

  requestId:
    string | null;
};

処理タイプごとに分けます。

operationの例
chat
summarize
classification
rag
code_generation
agent

同じモデルでも、分類とコード生成では必要な時間が大きく違います。

すべてのAPIを一つのLLM_TIMEOUT=30000で管理するより、処理種別ごとに設定します。

モデル別にもタイムアウトを分ける

軽量モデルと高性能Reasoningモデルに、同じタイムアウトを設定する必要はありません。

src/timeout-by-task.ts
const timeoutByTask = {
  classification:
    20_000,

  chat:
    45_000,

  rag:
    60_000,

  longArticle:
    180_000,

  agent:
    180_000,
} as const;

実際にはTaskだけでなくモデル、入力トークン、出力上限などもログへ残します。

モデルを変更した結果p95が20秒から40秒になった場合も、データから発見できます。

リトライ後の合計料金も保存する

APIがタイムアウトした場合でも、プロバイダー側ではモデル処理が進んでいた可能性があります。

そのため、再試行によって一つのユーザー操作から複数のAI API呼び出しが発生します。

リクエスト単位でトークン使用量をDBへ保存する方法で解説した料金記録では、最終的に成功したリクエストだけでなく、すべてのAttemptを同じtrace_idで保存します。

trace_idでの集約
trace_id: abc

attempt 1: OpenAI → timeout

attempt 2: OpenAI → success

これにより、

分析できる項目
再試行率

タイムアウト率

再試行で増えた料金

最終成功率

を分析できます。

タイムアウト値を10秒短くした結果、表面上の待ち時間は短くてもAPI料金が30%増えている可能性もあります。

タイムアウト時に別モデルへ切り替える

同じモデルで1回再試行しても失敗する場合は、別モデルまたは別プロバイダーへフォールバックできます。

フォールバックの流れ
OpenAI → timeout

OpenAI再試行 → timeout

Claude → success

ただし、全体の時間予算を維持します。

OpenAIを30秒×2回待ったあと、Claudeをさらに60秒待つような構成では、ユーザーは90秒以上待つことになります。

OpenAI・Claude・Gemini APIを自動で切り替える方法で解説したフォールバック設計と組み合わせ、

時間予算を継承する例
全体45秒

OpenAI: 最夥25秒

再試行: 残り時間内

Claude: 残り時間内

という考え方にします。

再試行回数より成功率を見る

「2回再試行する」という設定自体が目的ではありません。

重要なのは、再試行が本当に成功率を改善しているかです。

たとえば1か月のデータが、

値が小さい例
初回成功: 98.5%

1回目再試行で成功: 1.2%

2回目再試行で成功: 0.05%

全失敗: 0.25%

なら、2回目の再試行は待ち時間の割に効果が小さいかもしれません。

一方、

値が大きい例
初回成功: 95%

1回目再試行で成功: 4%

なら、一度の再試行には大きな価値があります。

実際の成功率と料金から回数を決めます。

タイムアウト値の初期設定

一般的なWebサービスでは、短い分類やStructured Outputsは15〜30秒程度から開始できます。

通常のAIチャットは30〜60秒程度、RAGや少し長い文章生成は45〜90秒程度が比較しやすい初期値です。

長文記事、コード生成、大きなコンテキスト、Reasoningなどは90〜300秒程度まで許可するか、Streamingへ移行します。

5分を超えることが正常な処理は、同期APIで待たずBackgroundまたはBatchへ移すことを検討します。

これらはモデル提供会社が保証するタイムアウト値ではありません。

自分のサービスのp95、p99、ユーザーが許容できる待ち時間、ホスティング環境の最大実行時間から調整します。

タイムアウトを決める順番

最初に、その処理がリアルタイムである必要があるかを確認します。

ユーザーが待っていないなら、長いタイムアウトを設定するよりBatchやBackgroundを利用します。

リアルタイム処理なら、正常リクエストの応答時間を測定します。

次にWebサーバーやプロキシの最大実行時間より短いLLM時間予算を設定します。

その後、一時エラーだけを1回程度再試行します。

運用開始後にp95、p99、タイムアウト率、再試行成功率、API料金を確認して値を調整します。

この順番なら、「何となく60秒」に固定する状況を避けられます。

LLM APIのタイムアウトに関するよくある質問

Q30秒と60秒ならどちらがおすすめですか

A短いチャットや分類なら30秒から試し、正常リクエストまでタイムアウトするなら45秒、60秒へ伸ばします。長文やReasoningでは60秒でも不足することがあるため、用途で分けます。すべてのAPIを一つの値へ統一しないことが重要です。

QOpenAI SDKの標準10分のままでもよいですか

Aバッチ処理や非常に長い処理では利用できる場合がありますが、一般的なWebチャットでは長すぎることがあります。さらにOpenAI SDKは標準で2回再試行するため、実際の完了まで10分を大きく超える可能性があります。Webアプリ側のSLAに合わせて明示的に変更します。

QClaudeも標準10分ですか

A2026年8月時点の公式TypeScript SDKでは標準10分で、maxRetriesは2です。Anthropicは長時間の非ストリーミングMessagesについて10分を超えないようSDK側でも検証し、長い処理にはStreamingを案内しています。

Q429は何回まで再試行すればよいですか

A固定回数だけで判断せず、Retry-Afterとエラーコードを確認します。短期的なRate Limitなら1〜2回程度から検証できます。Spend LimitやDaily Quotaに達した429は何度再試行しても直らないため、即座に停止します。

Q500エラーは何回でも再試行してよいですか

A一時障害なので再試行対象ですが、無制限には行いません。指数バックオフと最大回数を設定し、継続して失敗する場合はサーキットブレーカーや別プロバイダーへのフォールバックを使用します。

Qストリーミングならタイムアウトは不要ですか

A必要です。最初のイベントが来ない状態、途中でイベントが止まる状態、生成全体が異常に長い状態を検知する必要があります。通常APIの一つのタイムアウトではなく、初回応答、アイドル、全体時間に分ける方法が有効です。

まとめ

LLM APIのタイムアウトには、すべてのサービスで通用する一つの正解はありません。

一般的なWebアプリでは、短い分類やJSON生成を15〜30秒、通常チャットを30〜60秒、RAGや長文を45〜90秒、重いReasoningや長文生成を90〜300秒程度から検証できます。

ただし、これらは初期値です。

実際には正常リクエストのp95とp99を測定し、Webアプリ全体の最大実行時間より短く設定します。

OpenAIとClaudeのTypeScript SDKは標準タイムアウトが10分で、さらに標準2回の自動再試行があります。Webサービスではそのまま利用せず、timeoutmaxRetriesを明示しておくほうが挙動を把握しやすくなります。

再試行してよいのは、接続エラー、408、短期的な429、500、503、529などの一時障害です。

400、401、403、404、413などは入力、認証、権限、リソース、サイズの問題なので、同じ内容を自動再試行してはいけません。

429と409はHTTPステータスだけでは判断せず、Rate LimitなのかQuota枯渇なのか、競合なのか既存リソースなのかをエラーコードから確認します。

タイムアウトしたら無条件に何度も再試行するのではなく、アプリケーション全体に時間予算を設定し、その残り時間の中だけで再試行します。

数分以上掛かることが正常な処理はタイムアウトを延ばし続けるのではなく、Streaming、Background、Batchへ処理方式を変更することが、LLM APIを安定運用するうえで重要です。