OpenAI APIの429エラーを直す方法|Rate Limit・Retry-After・指数バックオフを実装

OpenAI APIの429エラーを直す方法|Rate Limit・Retry-After・指数バックオフを実装 AI開発

OpenAI APIを使っていると、突然「429 Too Many Requests」や「Rate limit reached」というエラーが発生することがあります。

429エラーは、単純にAPIを呼び出した回数が多い場合だけでなく、短時間に大量のトークンを送った場合や、プロジェクトの利用上限、残高不足によっても発生します。

そのため、429が返ってきたときに同じリクエストをすぐ送り直すだけでは解決できません。むしろ、失敗したリクエストもレート制限に加算されるため、無制限に再送するとエラーが長引く可能性があります。OpenAIは、429への基本的な対策としてRetry-Afterと指数バックオフを利用することを推奨しています。

この記事では、OpenAI APIの429エラーが発生する原因を整理したうえで、JavaScript・TypeScriptから安全にリトライする方法を解説します。TypeScriptでのOpenAI API利用そのものが初めての場合は、【TypeScript】OpenAI API入門を先に確認しておくと、環境構築や基本的な呼び出し方がつかみやすくなります。

スポンサーリンク

OpenAI APIの429エラーとは

HTTPステータスコード429は、APIへのリクエストが現在許可されている上限を超えたことを表します。

OpenAI APIでは、主にリクエスト数とトークン数に対して制限が設定されています。一定時間内に送信できるリクエスト数を超えた場合だけでなく、入力と出力で使用するトークン数が上限に達した場合にも429が返されます。

レート制限は組織やプロジェクト、使用モデル、利用ティアなどによって異なります。現在の上限はOpenAI Platformの設定画面にあるLimitsから確認できます。APIの利用実績が増えることで、利用ティアと多くのモデルのレート制限が自動的に引き上げられる場合もあります。

429エラーはすべてリトライすればよいわけではない

OpenAI APIの429には、大きく分けて一時的なレート制限と、設定変更や支払いが必要なエラーがあります。

一時的なレート制限では、エラーコードとしてrate_limit_exceededなどが返されます。この場合は、指定された時間だけ待ってから再実行することで成功する可能性があります。

一方、組織の利用金額上限に達した場合はorganization_spend_limit_exceeded、プロジェクトの上限に達した場合はproject_spend_limit_exceeded、プリペイド残高がなくなった場合はcredit_balance_exhaustedが返されます。

これらは時間を空けて再送しても直りません。組織やプロジェクトの利用上限を変更するか、残高を追加する必要があります。OpenAIの公式ドキュメントでも、Retry-Afterは一時的なレート制限に対して使われるものであり、課金や利用枠の問題をリトライで解決できるわけではないと説明されています。

まずエラーコードとリクエストIDを確認する

429が発生したら、ステータスコードだけで判断せず、エラーコードとリクエストIDをログに残します。

OpenAIのJavaScript・TypeScript SDKでは、APIが4xxまたは5xxを返したときにOpenAI.APIErrorのサブクラスがスローされます。429の場合はRateLimitErrorとして扱われ、statuscodeheadersrequest_idなどを確認できます。

src/generate-text.ts
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

async function generateText(): Promise<string> {
  const model = process.env.OPENAI_MODEL;

  if (!model) {
    throw new Error("OPENAI_MODELが設定されていません。");
  }

  try {
    const response = await client.responses.create({
      model,
      input: "TypeScriptの型ガードについて説明してください。",
    });

    return response.output_text;
  } catch (error) {
    if (error instanceof OpenAI.APIError) {
      console.error({
        status: error.status,
        code: error.code,
        type: error.type,
        requestId: error.request_id,
        message: error.message,
        headers: error.headers,
      });
    }

    throw error;
  }
}

request_idは、OpenAI側へ問い合わせる場合や、複数のAPIリクエストから問題のある通信を特定する場合に役立ちます。成功したレスポンスにも_request_idが付与されるため、本番環境では成功・失敗を問わず記録しておくと調査しやすくなります。

OpenAI公式SDKには自動リトライが搭載されている

公式のJavaScript・TypeScript SDKには、自動リトライ機能が最初から搭載されています。

現在のSDKでは、通信エラー、408、409、429、500番台のエラーが、短い指数バックオフを挟んでデフォルトで2回まで再試行されます。

簡単なアプリであれば、独自のリトライ処理を作らず、SDKのmaxRetriesを設定するだけでも対応できます。

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

const client = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  maxRetries: 5,
  timeout: 60_000,
});

この設定では、最初のリクエストに失敗したあと、最大5回まで再試行されます。

特定のAPI呼び出しだけリトライ回数を変更することもできます。

リクエスト単位の上書き
const response = await client.responses.create(
  {
    model: process.env.OPENAI_MODEL!,
    input: "指数バックオフについて説明してください。",
  },
  {
    maxRetries: 5,
    timeout: 60_000,
  },
);

公式SDKは、対象となるリトライでRetry-Afterヘッダーを考慮します。一般的なWebアプリであれば、まずSDK標準のリトライを利用し、それでも制御が足りない場合に独自実装へ切り替えるのが安全です。

Retry-Afterとは

一時的なレート制限が発生した場合、429レスポンスにRetry-Afterヘッダーが含まれることがあります。

たとえば、次のような値が返ってきた場合、少なくとも56秒待ってから再実行します。

レスポンスヘッダの例
Retry-After: 56

OpenAIの公式ドキュメントでは、Retry-Afterの値を最低待機時間として扱い、複数のクライアントが同時に再接続することを防ぐため、小さなランダム時間を追加することが推奨されています。

Retry-Afterがない場合は、指数バックオフを使って待機時間を徐々に増やします。

指数バックオフとは

指数バックオフは、リトライに失敗するたびに待機時間を増やす方法です。

最初は1秒、次は2秒、その次は4秒、8秒、16秒というように、待機時間を段階的に延ばします。

一定間隔で連続リトライすると、制限が解除される前に何度もリクエストを送り続けることになります。指数バックオフを使えば、APIへ負荷をかけながら失敗を繰り返す状況を避けられます。

OpenAIは、失敗したリクエストも1分あたりの制限に加算されるため、即時の連続再送は有効ではないと説明しています。また、レート制限は1分単位で表示されていても、内部では1秒などの短い単位に分割して適用される場合があります。そのため、平均値が上限以内でも、瞬間的にリクエストが集中すると429が発生します。

Retry-Afterと指数バックオフを使ったTypeScript実装

リトライの待機時間を完全に制御したい場合は、SDK標準の自動リトライを無効化し、独自の処理を実装します。

次のコードでは、一時的なrate_limit_exceededだけを再試行します。課金上限や残高不足による429は、そのままエラーとして終了します。

src/retry-with-backoff.ts
import OpenAI from "openai";

const model = process.env.OPENAI_MODEL;

if (!model) {
  throw new Error("OPENAI_MODELが設定されていません。");
}

const client = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  maxRetries: 0,
  timeout: 60_000,
});

const MAX_RETRIES = 5;
const BASE_DELAY_MS = 1_000;
const MAX_DELAY_MS = 30_000;
const JITTER_MS = 500;

const sleep = (milliseconds: number): Promise<void> =>
  new Promise((resolve) => setTimeout(resolve, milliseconds));

function readHeader(headers: unknown, name: string): string | null {
  if (!headers || typeof headers !== "object") {
    return null;
  }

  const headersObject = headers as {
    get?: (headerName: string) => string | null;
  };

  if (typeof headersObject.get === "function") {
    return headersObject.get(name);
  }

  for (const [key, value] of Object.entries(
    headers as Record<string, unknown>,
  )) {
    if (
      key.toLowerCase() === name.toLowerCase() &&
      typeof value === "string"
    ) {
      return value;
    }
  }

  return null;
}

function parseRetryAfterMilliseconds(value: string | null): number | null {
  if (!value) {
    return null;
  }

  const seconds = Number(value);

  if (Number.isFinite(seconds)) {
    return Math.max(0, seconds * 1_000);
  }

  const retryDate = Date.parse(value);

  if (Number.isNaN(retryDate)) {
    return null;
  }

  return Math.max(0, retryDate - Date.now());
}

function isTemporaryRateLimitError(
  error: unknown,
): error is OpenAI.APIError {
  if (!(error instanceof OpenAI.APIError)) {
    return false;
  }

  if (error.status !== 429) {
    return false;
  }

  return !error.code || error.code === "rate_limit_exceeded";
}

function calculateDelay(
  retryCount: number,
  retryAfterMilliseconds: number | null,
): number {
  const exponentialDelay = Math.min(
    MAX_DELAY_MS,
    BASE_DELAY_MS * 2 ** retryCount,
  );

  const minimumDelay = Math.max(
    retryAfterMilliseconds ?? 0,
    exponentialDelay,
  );

  const jitter = Math.random() * JITTER_MS;

  return minimumDelay + jitter;
}

export async function createResponseWithRetry(
  input: string,
): Promise<OpenAI.Responses.Response> {
  for (let retryCount = 0; ; retryCount += 1) {
    try {
      return await client.responses.create({
        model,
        input,
      });
    } catch (error) {
      if (!isTemporaryRateLimitError(error)) {
        throw error;
      }

      if (retryCount >= MAX_RETRIES) {
        console.error("OpenAI APIの最大リトライ回数に達しました。", {
          code: error.code,
          requestId: error.request_id,
          message: error.message,
        });

        throw error;
      }

      const retryAfterHeader = readHeader(
        error.headers,
        "retry-after",
      );

      const retryAfterMilliseconds =
        parseRetryAfterMilliseconds(retryAfterHeader);

      const delay = calculateDelay(
        retryCount,
        retryAfterMilliseconds,
      );

      console.warn("OpenAI APIのレート制限を検出しました。", {
        retryCount: retryCount + 1,
        waitMilliseconds: Math.round(delay),
        requestId: error.request_id,
        code: error.code,
      });

      await sleep(delay);
    }
  }
}

呼び出し側では、通常のResponses APIと同じように利用できます。

src/main.ts
async function main(): Promise<void> {
  try {
    const response = await createResponseWithRetry(
      "Node.jsで安全にAPIをリトライする方法を説明してください。",
    );

    console.log(response.output_text);
  } catch (error) {
    console.error("回答の生成に失敗しました。", error);
    process.exitCode = 1;
  }
}

void main();

このコードでは、SDK側のmaxRetriesを0にしています。

SDKの自動リトライを有効にしたまま外側にもリトライ処理を追加すると、実際のAPI呼び出し回数が想定より多くなる可能性があります。独自実装を使う場合は、どちらが再試行を担当するのかを明確にしておくことが重要です。

レート制限ヘッダーを確認する方法

OpenAI APIのレスポンスには、現在のレート制限を確認するためのヘッダーが含まれる場合があります。

x-ratelimit-limit-requestsはリクエスト数の上限、x-ratelimit-remaining-requestsは残りのリクエスト数、x-ratelimit-reset-requestsはリクエスト数の制限がリセットされるまでの時間を表します。

トークンについては、x-ratelimit-limit-tokensx-ratelimit-remaining-tokensx-ratelimit-reset-tokensを確認できます。

プロジェクト単位のトークン制限が適用される場合は、x-ratelimit-limit-project-tokensx-ratelimit-remaining-project-tokensx-ratelimit-reset-project-tokensが含まれることもあります。

OpenAIのJavaScript・TypeScript SDKでは、.withResponse()を使うことで、解析済みのデータと生のHTTPレスポンスを同時に取得できます。

src/check-headers.ts
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

const { data, response } = await client.responses
  .create({
    model: process.env.OPENAI_MODEL!,
    input: "レート制限について説明してください。",
  })
  .withResponse();

console.log({
  remainingRequests: response.headers.get(
    "x-ratelimit-remaining-requests",
  ),
  remainingTokens: response.headers.get(
    "x-ratelimit-remaining-tokens",
  ),
  resetRequests: response.headers.get(
    "x-ratelimit-reset-requests",
  ),
  resetTokens: response.headers.get(
    "x-ratelimit-reset-tokens",
  ),
  requestId: response.headers.get("x-request-id"),
});

console.log(data.output_text);

ヘッダーは常に同じ組み合わせで返されるとは限らないため、値がnullでも動作するように実装します。

RPMとTPMの違いを確認する

429が続く場合は、リクエスト数とトークン数のどちらを超えているのかを確認します。

RPMとTPMの違い

指標 正式名称 超えやすいケース
RPM Requests Per Minute(1分あたりのリクエスト数) 短いプロンプトを大量に並列送信する
TPM Tokens Per Minute(1分あたりのトークン数) 長いドキュメントや会話履歴を送信する

たとえば、1分間に10回しかAPIを呼び出していなくても、毎回非常に長い入力を送っていれば、リクエスト数より先にトークン数の上限へ達することがあります。

エラーメッセージやレート制限ヘッダーを確認し、RPMとTPMのどちらが不足しているかを切り分けることが重要です。

並列リクエストを減らす

指数バックオフは、429が発生したあとの回復処理です。429そのものを減らすには、短時間に集中するリクエストを制御する必要があります。

次のコードは、配列に含まれる処理を指定した並列数で実行する簡単なワーカープールです。

src/map-with-concurrency.ts
async function mapWithConcurrency<T, R>(
  items: readonly T[],
  concurrency: number,
  worker: (item: T, index: number) => Promise<R>,
): Promise<R[]> {
  if (!Number.isInteger(concurrency) || concurrency < 1) {
    throw new Error("concurrencyには1以上の整数を指定してください。");
  }

  const results = new Array<R>(items.length);
  let nextIndex = 0;

  async function runWorker(): Promise<void> {
    while (true) {
      const currentIndex = nextIndex;
      nextIndex += 1;

      if (currentIndex >= items.length) {
        return;
      }

      results[currentIndex] = await worker(
        items[currentIndex]!,
        currentIndex,
      );
    }
  }

  const workerCount = Math.min(concurrency, items.length);

  await Promise.all(
    Array.from({ length: workerCount }, () => runWorker()),
  );

  return results;
}

OpenAI APIへの呼び出しでは、次のように利用できます。

src/run-prompts.ts
const prompts = [
  "JavaScriptとは何ですか。",
  "TypeScriptとは何ですか。",
  "Node.jsとは何ですか。",
  "Reactとは何ですか。",
  "Next.jsとは何ですか。",
];

const responses = await mapWithConcurrency(
  prompts,
  2,
  async (prompt) => {
    const response = await createResponseWithRetry(prompt);
    return response.output_text;
  },
);

console.log(responses);

この例では、同時に実行されるAPIリクエストを2件までに制限しています。

ただし、並列数を制限するだけでは、1分あたりの総リクエスト数やトークン数を完全には制御できません。大量処理を行う場合は、キューへ投入して送信間隔を調整するか、Batch APIの利用も検討します。

Batch APIには通常のモデル別レート制限とは別の枠が用意されており、即時の応答が不要な大量処理に適しています。

入力トークンを減らす

TPMが原因の場合は、リトライ回数を増やすよりも、1回あたりの入力トークンを減らすほうが効果的です。

会話履歴を毎回すべて送信している場合は、古いメッセージを要約してから送ります。RAGを利用している場合は、検索結果の取得件数やチャンクサイズを見直します。

同じ長い指示文やドキュメントを繰り返し送信している場合は、プロンプトキャッシュを活用できる構成も検討します。

また、必要以上に長い回答を生成しないよう、出力トークン数の上限やプロンプトを調整することも重要です。

同じ内容を重複して送信しない

ユーザーが送信ボタンを連続で押したり、フロントエンドとバックエンドの両方で再送処理を行ったりすると、意図せず同じリクエストが複数回送信されます。

Reactなどの開発環境では、処理の呼び出し回数をログで確認し、同じイベントから複数のAPI通信が発生していないかを調べます。

送信中はボタンを無効化し、サーバー側でもリクエストIDやジョブIDを使って重複処理を防ぐ設計が有効です。

特にメール送信、データ登録、外部ツールの実行などを伴う処理では、APIレスポンスが届かなかったことと、処理自体が実行されなかったことは同じではありません。AIモデルへの問い合わせ以外の処理も再実行する場合は、冪等性を考慮する必要があります。

レート制限内なのに429が発生する理由

OpenAI Platformに表示される上限を超えていないように見えても、429が発生することがあります。

レート制限は必ずしも1分の最後にまとめて判定されるわけではありません。OpenAIは、たとえば60 RPMという制限が、実際には1秒あたり1リクエストのような短い時間単位で適用される場合があると説明しています。

そのため、1分間の平均が60回未満でも、1秒間に10回送信すると制限に達する可能性があります。

この問題は、アクセスが増えたWebアプリや、複数のサーバーレス関数が同時に起動する構成で発生しやすくなります。サーバーごとに個別のリトライ処理を持たせるだけでなく、Redisなどを使ってアプリケーション全体のキューやレートを共有する設計が必要になる場合があります。

利用上限そのものを引き上げる

指数バックオフや並列数の制限を実装しても、通常時のトラフィックがレート制限を超えている場合は根本的な解決になりません。

OpenAI PlatformのLimits画面で、現在の利用ティア、モデルごとの制限、組織やプロジェクトの上限を確認します。

支払い実績が増えると利用ティアが自動的に上がり、多くのモデルでレート制限が引き上げられる場合があります。ただし、組織やプロジェクトに手動で低い利用上限を設定している場合は、その設定も確認する必要があります。

429エラー対策で避けたい実装

429を捕捉した直後に、待機せず同じリクエストを送り直す実装は避けます。

次のような無制限ループでは、成功するまで高速でAPIを呼び続けます。

NG:待機なしの即時リトライ
while (true) {
  try {
    return await client.responses.create({
      model,
      input: "回答を生成してください。",
    });
  } catch {
    // 待機せずに即時リトライしている
  }
}

失敗した通信もレート制限に加算されるため、この処理は状況を悪化させます。

リトライには必ず最大回数を設定し、Retry-Afterまたは指数バックオフに基づく待機を入れます。最大回数に達したら、ユーザーへ時間を空けて再実行するよう案内するか、ジョブをキューへ戻します。

また、支払い上限や残高不足など、時間経過で直らない429をリトライし続けないようにします。

本番環境で記録しておきたい情報

本番環境では、429が発生した回数だけでなく、エラーコード、使用モデル、リトライ回数、待機時間、リクエストIDを記録します。

可能であれば、残りのリクエスト数、残りのトークン数、各制限のリセット時間も記録します。

ただし、プロンプト本文やAPIレスポンスをそのままログへ保存すると、個人情報や機密情報が残る可能性があります。入力内容ではなく、文字数、推定トークン数、処理種別、ユーザーや機能を匿名化した識別子などを記録する設計が安全です。

OpenAI SDKのデバッグログではHTTPリクエストやレスポンスのヘッダーと本文が出力される場合があるため、本番環境で詳細ログを有効にするときは注意が必要です。

OpenAI APIの429エラーに関するよくある質問

Q429が出たら、とにかくすぐ再試行すればいい?

Aいいえ。まずエラーコードを確認してください。rate_limit_exceededのような一時的なレート制限であれば待機後の再試行で解決しますが、organization_spend_limit_exceededcredit_balance_exhaustedのような課金・残高系のエラーは、時間を空けても直りません。上限の引き上げや残高の追加が必要です。

Q自作のリトライ処理を書かず、SDK標準の自動リトライだけで十分?

A小〜中規模のアプリであれば、maxRetriesを設定するだけで多くの場合は十分です。ただし、429の理由による切り分け(課金系は再試行しない等)や、独自のログ記録、Retry-Afterの詳細な制御が必要な場合は、SDKの自動リトライを無効化(maxRetries: 0)して独自実装に切り替えることを検討してください。

QRetry-Afterヘッダーがないときはどうすればいい?

ARetry-Afterが含まれない場合は、指数バックオフ(1秒→2秒→4秒…と待機時間を段階的に増やす方法)を使います。あわせて、同時に再接続するクライアントが重ならないよう、小さなランダムなジッターを加えるのが安全です。

Qレート制限内のはずなのに429が出るのはなぜ?

Aレート制限は1分単位の平均ではなく、内部的にはより短い時間単位(たとえば1秒あたり1リクエストなど)で適用される場合があります。1分間の合計が上限以内でも、瞬間的にリクエストが集中すると429が発生することがあります。並列数を制限するか、送信間隔を調整することで緩和できます。

まとめ

OpenAI APIの429エラーが発生したら、最初にエラーコードを確認し、一時的なレート制限なのか、利用金額上限や残高不足なのかを切り分けます。

一時的なrate_limit_exceededであれば、Retry-Afterを最低待機時間として利用し、値がない場合は指数バックオフとランダムなジッターを使って再試行します。

簡単な構成では、OpenAI公式SDKに搭載されている自動リトライを利用できます。独自のリトライ処理を実装する場合は、SDKのmaxRetriesを0にして、二重に再試行されないようにします。

429が頻繁に発生する場合は、リトライだけで解決しようとせず、並列数、送信間隔、入力トークン数、重複リクエスト、組織やプロジェクトの利用上限を見直すことが重要です。基本的なAPIの使い方から見直したい場合は、【TypeScript】OpenAI API入門もあわせてご覧ください。