GASでClaude APIの429エラーに強い
自動リトライを実装する方法

スプレッドシートの大量データをClaude APIで処理すると、途中で429(レート制限)やタイムアウトで止まりがちです。指数バックオフ+ジッターの自動リトライで、大量処理でも止まらず完走させる方法を動くコードで解説します。

|対象: GAS / Claude API / エラーハンドリング

Table of Contents

429・タイムアウトはなぜ起きるのか

Claude APIには、1分あたりに送れるリクエスト数やトークン数の上限(レート制限)があります。数百行のスプレッドシートをループで一気に処理すると、この上限を超えて429(Too Many Requests=送りすぎ)が返ります。単発で見れば異常ではなく、少し待って送り直せば通ることがほとんどです。

429 レート制限

短時間にリクエストを送りすぎて上限に達した。待てば回復する一時的なエラー

529 / 500系

サーバー側が一時的に混雑・過負荷。これも再送で成功することが多い

タイムアウト

GASのUrlFetchAppは応答が返らないと例外を投げる。長文処理で起きやすい

401 / 400

APIキー誤りやリクエスト不正。これはリトライしても直らないので即停止

つまり大切なのは「待って再送すべきエラー」と「即停止すべきエラー」を見分け、前者だけを賢く再送することです。まずはClaude APIを呼び出す基本形から確認します。

// APIキーはコードに直書きせず、スクリプトプロパティに保存しておく
// プロジェクトの設定 > スクリプト プロパティ に CLAUDE_API_KEY を登録
function callClaude(userText) {
  const apiKey = PropertiesService.getScriptProperties()
    .getProperty("CLAUDE_API_KEY");

  const payload = {
    model: "claude-haiku-4-5",
    max_tokens: 512,
    messages: [{ role: "user", content: userText }],
  };

  const res = UrlFetchApp.fetch("https://api.anthropic.com/v1/messages", {
    method: "post",
    contentType: "application/json",
    headers: {
      "x-api-key": apiKey,
      "anthropic-version": "2023-06-01",
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true, // ← これが重要(後述)
  });

  const code = res.getResponseCode();
  const body = JSON.parse(res.getContentText());

  if (code === 200) {
    return body.content[0].text;
  }
  throw new Error("Claude API error " + code + ": " + res.getContentText());
}

muteHttpExceptionsでステータスを自分で判定する

リトライ処理の前提になるのがmuteHttpExceptions: true(HTTPエラーで例外を投げない設定)です。これを付けないと、200以外のステータスが返った瞬間にUrlFetchApp.fetch()が例外を投げ、コードで待機・再送する前に処理が止まってしまいます。付けておけば、返ってきたステータスコードを自分で見て分岐できます。

// muteHttpExceptions を付けないと、200以外のとき
// UrlFetchApp.fetch() が即座に例外を投げてしまい、
// ステータスコードを見て分岐する前に処理が止まってしまう。

// 付けた場合は、200でも429でも例外を投げずにレスポンスを返すので、
// getResponseCode() で自分で判定できる。
function classifyStatus(code) {
  if (code === 200) return "ok";        // 成功
  if (code === 429) return "retry";     // レート制限 → 待って再送
  if (code >= 500) return "retry";      // サーバー側の一時エラー → 再送
  if (code === 401 || code === 403) return "fatal"; // 認証エラー → 停止
  if (code >= 400) return "fatal";      // リクエスト不正 → 停止
  return "retry";
}

このように、ステータスコードを「再送する(retry)」「即停止する(fatal)」に振り分ける関数を1つ用意しておくと、後のリトライ処理がすっきりします。

指数バックオフ+ジッターで自動リトライする

再送は「一定間隔で何度も送る」よりも、待機時間を1秒→2秒→4秒→8秒と倍々に伸ばす指数バックオフ(リトライのたびに待ち時間を倍にする方式)が効果的です。混雑しているサーバーに一斉再送で追い打ちをかけるのを防げます。さらに待機時間に少しランダムなズレを足すジッター(待機時間の揺らぎ)を加えると、複数の処理が同じ瞬間に再送するのを避けられます。

// 指数バックオフ+ジッター付きの自動リトライ
function callClaudeWithRetry(userText, maxRetries) {
  maxRetries = maxRetries || 5;
  const apiKey = PropertiesService.getScriptProperties()
    .getProperty("CLAUDE_API_KEY");

  const options = {
    method: "post",
    contentType: "application/json",
    headers: {
      "x-api-key": apiKey,
      "anthropic-version": "2023-06-01",
    },
    payload: JSON.stringify({
      model: "claude-haiku-4-5",
      max_tokens: 512,
      messages: [{ role: "user", content: userText }],
    }),
    muteHttpExceptions: true,
  };

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const res = UrlFetchApp.fetch(
      "https://api.anthropic.com/v1/messages",
      options
    );
    const code = res.getResponseCode();

    if (code === 200) {
      return JSON.parse(res.getContentText()).content[0].text;
    }

    const kind = classifyStatus(code);
    if (kind === "fatal" || attempt === maxRetries) {
      throw new Error("Claude API error " + code + ": " + res.getContentText());
    }

    // 待機時間 = 基本待機(1秒) × 2^attempt + ジッター
    const base = 1000 * Math.pow(2, attempt); // 1s, 2s, 4s, 8s, 16s
    const jitter = Math.floor(Math.random() * 500); // 0〜0.5秒のズレ
    const waitMs = Math.min(base + jitter, 32000);   // 上限32秒
    Logger.log("attempt " + attempt + " failed (" + code + "). wait " + waitMs + "ms");
    Utilities.sleep(waitMs);
  }
}

最大リトライ回数を超えたら例外を投げて呼び出し元に知らせます。Math.minで1回あたりの待機時間に上限(例では32秒)を設けているのは、待機の合計がGASの実行時間制限を圧迫しすぎないための保険です。

Retry-Afterヘッダーを尊重する

429が返るとき、レスポンスヘッダーにretry-after(「何秒後に再送してよいか」を示す秒数)が含まれることがあります。サーバーが「これだけ待って」と教えてくれている値なので、あればそれを優先し、無ければ指数バックオフにフォールバックするのが最も行儀の良い実装です。

// 429のとき、レスポンスヘッダーに retry-after(秒数)が
// 入っていることがある。あればその値を優先して待つ。
function getWaitMs(res, attempt) {
  const headers = res.getAllHeaders();
  // ヘッダー名は環境によって大文字小文字が異なるため両方見る
  const retryAfter = headers["retry-after"] || headers["Retry-After"];

  if (retryAfter) {
    const sec = parseInt(retryAfter, 10);
    if (!isNaN(sec)) {
      return Math.min(sec * 1000 + 250, 32000);
    }
  }

  // ヘッダーが無ければ指数バックオフにフォールバック
  const base = 1000 * Math.pow(2, attempt);
  const jitter = Math.floor(Math.random() * 500);
  return Math.min(base + jitter, 32000);
}

getAllHeaders()で取得できるヘッダー名は環境により大文字小文字が異なる場合があるため、両方の表記を確認しておくと取りこぼしを防げます。先ほどのリトライ関数の待機計算部分を、このgetWaitMs(res, attempt)に差し替えれば完成です。

fetchAllでまとめ送りしつつ頻度を抑える

件数が多いときは、1件ずつfetch()するよりUrlFetchApp.fetchAll()(複数リクエストをまとめて送るメソッド)でまとめて送ると、通信の待ち時間を短縮できます。ただし一度に大量に送ると逆に429を誘発するため、送る本数(chunk)を小分けにし、chunk間に間隔を空けるのがコツです。

// fetchAll なら複数リクエストを1回のまとめ呼び出しで送れる。
// ただし一度に送りすぎると429を誘発するので、chunkで小分けにする。
function callClaudeBatch(texts, chunkSize) {
  chunkSize = chunkSize || 5; // 一度に送る本数を絞る
  const apiKey = PropertiesService.getScriptProperties()
    .getProperty("CLAUDE_API_KEY");
  const results = [];

  for (let i = 0; i < texts.length; i += chunkSize) {
    const chunk = texts.slice(i, i + chunkSize);

    const requests = chunk.map((text) => ({
      url: "https://api.anthropic.com/v1/messages",
      method: "post",
      contentType: "application/json",
      headers: {
        "x-api-key": apiKey,
        "anthropic-version": "2023-06-01",
      },
      payload: JSON.stringify({
        model: "claude-haiku-4-5",
        max_tokens: 512,
        messages: [{ role: "user", content: text }],
      }),
      muteHttpExceptions: true,
    }));

    const responses = UrlFetchApp.fetchAll(requests);

    responses.forEach((res, j) => {
      if (res.getResponseCode() === 200) {
        results.push(JSON.parse(res.getContentText()).content[0].text);
      } else {
        // 失敗した1件だけ、単発のリトライ関数で送り直す
        results.push(callClaudeWithRetry(chunk[j]));
      }
    });

    Utilities.sleep(1000); // chunk間に間隔を空けて頻度を下げる
  }
  return results;
}

まとめ送りの中で失敗した1件だけを、先ほどの単発リトライ関数callClaudeWithRetryで送り直しています。「速いまとめ送り」と「堅牢な単発リトライ」を組み合わせる形です。

429を根本的に減らす送信設計

リトライはあくまで最後の保険です。そもそも429を起こしにくい送り方にしておくと、処理は速く安定します。

1. 複数行を1リクエストにまとめる

10行を10回に分けて送るのではなく、「次の10件を分類して」と1回のプロンプトにまとめ、JSON配列で結果を受け取れば、リクエスト数が10分の1になります。リクエスト数がそのまま429の起きやすさに直結します。

2. 利用プランのレート上限を把握する

1分あたりのリクエスト数・トークン数の上限は利用プランによって異なります。上限を確認し、その範囲に収まる送信ペース(chunkサイズと待機時間)に調整します。

3. 用途に合ったモデルを選ぶ

分類やタグ付けのような軽い処理なら、軽量モデル(例:claude-haiku-4-5)で十分なことが多く、レスポンスが速い分だけ全体の処理も安定します。

実務での注意点

1. 待機時間も6分制限に含まれる

Utilities.sleepの待機時間もGASの実行時間(1回最大6分)に加算されます。リトライ回数と上限待機時間を決め、全体で数十秒に収めましょう。件数が多い場合は時間主導トリガーで分割実行と組み合わせます。

2. どこまで処理したかを記録する

途中で実行が止まっても再開できるよう、処理済みの行にフラグ列を立てておきます。次回実行時は未処理の行だけを対象にすれば、同じ行への二重課金も防げます。

3. 失敗の記録を残す

最大リトライを使い切っても失敗した行は、エラー内容を別セルやログに残します。無言で飛ばすと、後から「なぜこの行だけ空欄なのか」が追えなくなります。

まとめ

GASからClaude APIを大量に呼び出すときは、muteHttpExceptionsでステータスを自分で判定し、429やサーバーエラーだけを指数バックオフ+ジッターで再送するのが定石です。Retry-Afterヘッダーを尊重し、fetchAllで頻度を抑えつつ、そもそも429を起こしにくい送信設計にしておけば、大量処理でも止まらず完走できます。リトライ・分割実行・処理済み記録の3つを揃えることが、安定運用の要です。

よくある質問

429は「Too Many Requests」を表すHTTPステータスコードで、短時間にリクエストを送りすぎてレート制限(1分あたりのリクエスト数やトークン数の上限)に達したときに返ります。エラーそのものは異常ではなく、少し待ってから送り直せば成功することがほとんどです。GAS側で自動的に待機・再送する仕組みを入れておくと、大量処理でも止まらずに完走できます。

リトライのたびに待機時間を2倍ずつ伸ばしていく方式です。1秒→2秒→4秒→8秒のように待つことで、サーバーが混雑しているときに全員が一斉に再送して混雑を悪化させる事態を防ぎます。さらに待機時間に少しランダムなズレ(ジッター)を足すと、複数の処理が同じタイミングで再送するのを避けられます。

待機時間もGASの実行時間(1回あたり最大6分)に含まれるため、リトライを重ねすぎると制限に達します。最大リトライ回数と1回あたりの上限待機時間を決め、待機の合計が数十秒に収まるよう設計するのが安全です。処理件数が多い場合は、1回の実行で全件を処理せず、時間主導トリガーで分割実行と組み合わせます。

429(レート制限)や500・529などのサーバー側の一時的なエラーはリトライする価値があります。一方、400(リクエストの不正)や401(APIキー誤り)はリトライしても同じ結果になるため、即座に停止して原因を直すべきです。ステータスコードで再送すべきかどうかを分岐させるのがポイントです。

リトライは最後の保険です。まずは送信間隔を空ける、複数行をまとめて1回のリクエストに含める、UrlFetchApp.fetchAllで並列送信しつつ一度に送る本数を絞る、といった設計でリクエスト頻度そのものを下げることが重要です。利用プランのレート上限を把握し、その範囲に収まる送信ペースにするのが基本です。

AI×GASの業務自動化を
相談する。

Claude APIを使った大量データ処理は、リトライ設計や分割実行を含めて安定運用まで見据えた実装が重要です。要件整理から運用設計までご相談いただけます。

AI×GAS自動化を見る