529 Overloadedと429:AIのAPIが落ちる前提で組む
サーバー混雑の529と、自分の枠を使い切った429は、同じリトライで扱ってはいけません。日次バッチが2日分止まった実データと、そのあと書いたリトライ実装のコードを載せます。
AIのAPIは落ちます。落ちること自体は避けられないので、落ちる前提でどう組むかの話をします。
この記事では、私が実際に踏んだ2種類の障害を扱います。片方は数分で復旧しましたが、もう片方は日次バッチを2日分、約55時間止めました。両方とも実際のエラー文とログを載せます。
まず、2種類の「落ちる」を区別してください
これが一番大事です。
| 529 Overloaded | 429 Too Many Requests | |
|---|---|---|
| 意味 | サーバー側が混雑している | 自分の枠を使い切った |
| 原因 | 相手の都合 | 自分の使い方 |
| 時間が解決するか | する | 枠の種類による(下記) |
| 正しい対処 | 待って再試行 | 短期の枠なら待つ/日次の枠なら設計を変える |
429はさらに2種類に分かれます。ここを混ぜると事故ります。
| 429の種類 | 何を超えたか | 待てば直るか |
|---|---|---|
| 短期のレート制限 | 1分あたりのリクエスト数など | 直る(数秒〜数十秒) |
| 日次クォータの枯渇 | 1日あたりの上限 | 直らない(リセットまで待てない) |
同じ429でも対処が正反対です。 短期のレート制限なら、サーバーが指定してくる秒数だけ待てば通ります。一方、1日の枠を使い切っている場合は、何度待って再試行しても枠がリセットされるまで通りません。待った時間だけ無駄になります。
この記事でバッチを止めたのは、後者の日次クォータの枯渇です。 以下、それぞれ実物を見ていきます。
529 Overloaded:待てば直る
Claude Codeを使っていて出たエラーの実物です。
API Error: 529 Overloaded. This is a server-side issue, usually temporary
— try again in a moment. If it persists, check https://status.claude.com.
2026年7月25日13時34分、3つのエージェントを並列で走らせている最中に出ました。エラーメッセージ自体が “usually temporary”(たいてい一時的)と言っています。
このときの実際の推移です。
13:34 API Error: 529 Overloaded.
13:35 (人間)何かありました?
13:40 進捗をお伝えします。問題は出ていません。3段のうち2段が終わりました。
6分後には何事もなく続きが動いていました。 529については、基本的にこれで終わりです。
やることは3つだけです。
- 即座に再試行しない。 少し間を空けます
- 長時間タスクは区切りをつけておく。 中断されても被害が小さくなります
status.claude.comを見る。 自分の問題か相手の問題かが分かります
同時に複数セッションを走らせているときは遭遇率が上がりますが、これは待てば済みます。問題は次の429です。
429:日次バッチが2日分、約55時間止まりました
毎日AIのニュースを取得して要約し、通知するバッチをGitHub Actionsで動かしています。要約にはGeminiのAPIを使っています。
これが2日連続で失敗しました。実行履歴の実データです。
2026-07-30T22:34 success
2026-07-31T22:31 failure ← ここから
2026-08-01T22:25 failure ← 2日連続
2026-08-02T05:45 failure (手動で再実行)
2026-08-02T05:47 failure (もう一度)
2026-08-02T05:49 success ← 復旧
気づくのに2日かかっています。 失敗しても通知が飛ばない作りだったからです。
エラーの実物
失敗したジョブのログから抜き出したものです。
警告: バッチ 2 の分類に失敗しました: Gemini API(分類) 429: {
"error": {
"code": 429,
"message": "You exceeded your current quota, please check your plan and
billing details. For more information on this error, head to:
https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current
usage, head to: https://ai.dev/rate-limit. \n* Quota ex
You exceeded your current quota — 1日あたりの枠を使い切っていました。 前述の2種類のうち、待っても直らないほうです。
公式ドキュメントで確認できたこと
Googleの公式ドキュメントで、次の3点を確認しました。
Rate limits are applied per project, not per API key. Requests per day (RPD) quotas reset at midnight Pacific time.
(出典: Gemini API — Rate limits)
- レート制限はAPIキー単位ではなくプロジェクト単位。 キーを増やしても枠は増えません
- 1日あたりの枠のリセットは太平洋時間の深夜。 自分のタイムゾーンの「今日」とはズレます
- 無料枠の具体的な数値は、このドキュメントに載っていません。 AI Studioのダッシュボードで確認する必要があります
2番は実務で効きます。日本時間で夜に走らせるバッチは、太平洋時間では昼間です。「日付が変わったからリセットされているはず」という直感は通りません。
書いたリトライ実装
復旧後、リトライとフォールバックを実装しました。実際のコードです。
async function callGeminiWithRetry(
body: GeminiRequestBody,
models: string[],
label: string
): Promise<GeminiResponse> {
for (let modelIndex = 0; modelIndex < models.length; modelIndex++) {
const model = models[modelIndex];
let retries429 = 0;
let retries503 = 0;
let removedThinkingConfig = false;
while (true) {
const response = await fetch(endpoint, { /* ... */ });
if (response.ok) return await response.json();
const errorBody = await response.text();
// 400: モデルが特定の設定を受け付けない場合がある。外して1回だけ再試行
if (response.status === 400 && !removedThinkingConfig) {
requestBody = withoutThinkingConfig(requestBody);
removedThinkingConfig = true;
continue;
}
// 429: サーバーが「何秒待て」と言ってくるので、それに従う
if (response.status === 429 && retries429 < 2) {
retries429++;
await sleep(retryDelayMs(errorBody));
continue;
}
// 503: 固定のバックオフ
if (response.status === 503 && retries503 < 3) {
await sleep([5_000, 15_000, 45_000][retries503]);
retries503++;
continue;
}
// それ以外は即座に諦める
if (response.status !== 429 && response.status !== 503) {
throw new Error(`Gemini API(${label})${response.status}: ${errorBody}`);
}
break; // ここを抜けると次のモデルへ
}
}
}
意図を4点に分けて説明します。
1. 待ち時間は自分で決めず、サーバーに聞く
429のレスポンスには、Googleが「何秒後に再試行しろ」という値を入れてきます。これを読みます。
function retryDelayMs(errorBody: string): number {
const parsed = JSON.parse(errorBody);
const details = parsed?.error?.details;
if (!Array.isArray(details)) return 15_000;
for (const detail of details) {
const retryDelay = detail?.retryDelay; // 例: "23s"
if (typeof retryDelay !== "string") continue;
const match = /^(\d+(?:\.\d+)?)s$/.exec(retryDelay);
if (match) return Math.ceil(Number(match[1]) * 1000);
}
return 15_000; // 読めなければ15秒
}
固定の秒数で待つより、サーバーの指示に従うほうが確実です。 読めなかったときのために既定値も置いています。
2. ステータスコードごとに戦略を変える
| コード | 戦略 | 理由 |
|---|---|---|
| 400 | 設定を1つ外して1回だけ再試行 | モデルによって受け付けるパラメータが違うため |
| 429 | サーバー指示の秒数で最大2回 | 短期のレート制限なら通る。日次の枯渇なら粘っても無駄 |
| 503 | 5秒 → 15秒 → 45秒で最大3回 | 一時的な過負荷なので、粘る価値がある |
| その他 | 即座に諦める | リトライで直る種類ではない |
429で「2回だけ」なのが要点です。 短期のレート制限なら、サーバー指定の秒数を待つ2回で通ります。それで駄目なら日次の枠を使い切っている可能性が高いので、粘らずに次の手(モデルの乗り換え)へ移ります。
3. 全部駄目なら、モデルを乗り換える
枠はモデルごとに別勘定です。あるモデルで使い切っていても、別のモデルはまだ残っています。
const MODELS = (process.env.MODEL_FALLBACK
?? "gemini-3-flash-preview,gemini-3.5-flash,gemini-2.5-flash").split(",");
環境変数で順序を差し替えられるようにしました。この乗り換えが復旧につながったと考えています。
復旧時刻の実データです。GitHubのリポジトリ変数に記録が残っています。
MODEL gemini-3-flash-preview 2026-08-02T05:49:13Z
そしてワークフローの実行履歴は 2026-08-02T05:49 success です。変数を切り替えた直後の実行が通りました。(時系列としてはこの通りですが、切り替えが成功要因だったことを分離して検証はしていません。推測を含みます)
4. 全滅しても、配信自体は止めない
一番効いたのはこれです。
以前の実装は、要約に失敗すると例外を投げて全体が止まっていました。ニュースを取得して、分類して、要約して、通知する。この一直線の途中で1つ落ちると、何も届きません。
変更後は、分類が全滅しても未分類のまま配信を続けます。
if (geminiStats.classificationFallback) {
notices.push("分類に失敗したため未分類で配信しています");
}
「劣化して動く」と「止まる」の差は大きいです。 分類が無くても、その日のニュース一覧は届きます。届けば異常にも気づけます。
ログを300文字で切ったせいで、原因が読めませんでした
実装の話をもう1つ。当時のコードはこう書いていました。
throw new Error(`Gemini API(${label})${response.status}: ${errorBody.slice(0, 300)}`);
エラー本文を300文字で切っています。長いログを避けるつもりでした。
その結果、実際のログはこうなりました。
"message": "You exceeded your current quota, please check your plan and billing
details. For more information on this error, head to: https://ai.google.dev/
gemini-api/docs/rate-limits. To monitor your current usage, head to:
https://ai.dev/rate-limit. \n* Quota ex
Quota ex で切れています。 この続きに「どのクォータを、いくつ超えたのか」が書かれていました。切ったせいで、ログだけでは原因が特定できませんでした。
エラー本文は切らないでください。 特にクォータやレート制限のエラーは、肝心の情報が後ろに来ます。長さが気になるなら、正常系のログを削るほうが先です。
おまけ:スケジュール実行の時刻は当てにならない
このバッチのcron設定は 21:26 UTC です。ところが実際の実行時刻は 22:25 22:31 22:34 でした。1時間近く遅れています。
GitHub Actionsのスケジュール実行は、混雑時に遅延・スキップされます。ワークフローには次のコメントを入れてあります。
schedule:
# 毎時0分・30分ちょうどはGitHub Actions側が混雑し遅延・スキップされやすいため、
# あえて半端な分にずらしている
- cron: "26 21 * * *"
半端な分にずらしても、遅延はします。即時性が要件になるなら、GitHub Actions以外のスケジューラを検討してください。
そして遅延した結果、太平洋時間での「その日」がズレる可能性があります。1日単位の枠を使う処理では、これが429の引き金になり得ます。
設計として残したこと
今回の障害から、次の4つを運用ルールにしました。
1. 529と429を分けて扱う 待てば直るものと、待っても直らないものを、同じ処理に流さない。
2. 待ち時間はサーバーに聞く
retryDelay を返してくるAPIなら、それに従う。固定値は読めなかったときの保険にする。
3. 部分的に失敗しても、届くものは届ける 一直線のパイプラインを組まない。途中が落ちても、劣化した形で最後まで通す。
4. エラー本文を切らない 原因は後ろに書いてあることが多い。
そして一番重要なのは、失敗したことに気づける状態を作ることです。今回は約55時間(日次実行2回分)気づきませんでした。実行履歴を人間が見に行かないと分からない作りだったからです。
まとめ
529は待てば直ります。エラーメッセージ自体がそう書いていますし、実際6分で復旧しました。構えるべきなのは429のほうです。
枠を使い切った状態は、リトライでは絶対に直りません。直すのは設計です。モデルを乗り換える経路を用意しておく、失敗しても劣化して動くようにしておく、そして失敗に気づける状態にしておく。
Claude Codeの導入と基本設定は個人開発者のためのClaude Code完全ガイドに、Codexとの併用は役割分担の実践論にまとめています。
この記事の実測データ
| 項目 | 内容 |
|---|---|
| 529の遭遇 | 2026-07-25 13:34(Claude Code・3セッション並列中) |
| 429による停止 | 2026-07-31 〜 2026-08-02(GitHub Actions・日次バッチ) |
| 復旧 | 2026-08-02 05:49 UTC(モデル切り替え直後) |
| リトライ実装 | 2026-08-03 00:28 JST にコミット |
| 対象API | Gemini API(無料枠) |