GEMINI LABEN
API — Gemini 3.5 Flashが一般提供。gemini-flash-latestの実体となり、エージェント・コーディングで持続的に高性能を出しますAGENT — Managed Agentsが公開プレビュー。Googleホストの隔離Linuxサンドボックスでステートフルな自律エージェントを構築できますSEARCH — File Searchがマルチモーダル対応。gemini-embedding-2で画像をそのまま埋め込み・検索できますRESEARCH — Deep Researchの新版が協調プランニング・可視化・MCPサーバー連携・File Searchに対応しましたSHEETS — Gemini in Sheetsが周辺データ構造を解析し、数式エラーを1クリックで診断・修正しますROADMAP — Gemini 3.5 Proは品質調整のため7月へ延期。最新の主力はFlash系ですAPI — Gemini 3.5 Flashが一般提供。gemini-flash-latestの実体となり、エージェント・コーディングで持続的に高性能を出しますAGENT — Managed Agentsが公開プレビュー。Googleホストの隔離Linuxサンドボックスでステートフルな自律エージェントを構築できますSEARCH — File Searchがマルチモーダル対応。gemini-embedding-2で画像をそのまま埋め込み・検索できますRESEARCH — Deep Researchの新版が協調プランニング・可視化・MCPサーバー連携・File Searchに対応しましたSHEETS — Gemini in Sheetsが周辺データ構造を解析し、数式エラーを1クリックで診断・修正しますROADMAP — Gemini 3.5 Proは品質調整のため7月へ延期。最新の主力はFlash系です
記事一覧/API / SDK
API / SDK/2026-06-27上級

Gemini API の 429 を全部リトライしてはいけません — レート制限と Spend Cap 枯渇を見分けるリトライ設計

429 RESOURCE_EXHAUSTED には『1秒待てば直る』ものと『今月はもう叩いても無駄』なものが混ざっています。Project Spend Caps の一般提供で後者が現実的になった今、両者を分類してリトライ層とサーキットブレーカーを設計する方法を実装込みでまとめます。

gemini-api253rate-limit4retry3spend-capproduction95

プレミアム記事

個人開発で運営している壁紙アプリの裏側で Gemini を回していると、429 RESOURCE_EXHAUSTED は珍しいエラーではありません。問題は、この 429 が二種類あることに長いあいだ気づいていなかったことでした。ひとつは「同じ秒に投げすぎた」一過性のレート制限で、数百ミリ秒待てば素直に通ります。もうひとつは「このプロジェクトの今月の予算をもう使い切った」枯渇で、こちらは何秒待っても、何回叩いても、月が変わるまで通りません。

両方を同じ指数バックオフで処理していると、後者のときにリトライ層が静かに暴れます。1リクエストにつき7回まで再試行する設定なら、枯渇したプロジェクトに対してアプリが延々と7倍の無駄打ちを続け、ユーザーから見れば「読み込みが異常に遅いだけのアプリ」になります。AdMob の広告収益で回している無料アプリだと、この遅延はそのまま離脱につながります。

2026年6月26日に Project Spend Caps が一般提供になり、プロジェクト単位で月間ドル上限を設定できるようになりました。これは費用を構造的に止められる嬉しい更新ですが、同時に「上限に当たった 429」を本番で踏む確率を確実に上げます。つまり、429 を一律でリトライする設計は、いまこそ見直しどきです。プロジェクト構成そのものでの分離はSpend Cap の影響範囲をティア別に分ける設計で扱っているので、本記事はリクエスト時の縮退に絞ります。

429 を「待てば直る」と「待っても無駄」に二分する

最初にやるべきは、リトライ層に渡る前に 429 を分類することです。判断材料は大きく3つあります。

ひとつ目は、エラーレスポンスに含まれる google.rpc.RetryInfo です。サーバーが「この時間だけ待ってから再試行してよい」と明示している場合、retryDelay フィールドが入ってきます。これが付いている 429 は、設計上リトライしてよいレート制限だと解釈できます。

ふたつ目は QuotaFailure の詳細で、どのクォータ次元(リクエスト毎分・トークン毎分など)に当たったかが分かります。秒・分単位で回復するクォータなら待てば直りますが、日次や月次の上限に当たっているなら、待ち時間の単位がまるで違います。

みっつ目が、自分でしか持っていない情報、つまり自前の月次支出ゲートです。これが最も重要です。Spend Cap に当たったかどうかを API のエラー本文だけから確実に判定しようとすると、エラー形状の細部に依存した脆い実装になります。代わりに、自分の側で「今月いくら使ったか」を概算で持っておき、その数字を分類の主軸に据えます。API はあくまで補助信号として使います。

信号意味リトライ判断
RetryInfo.retryDelay ありサーバー指定の待機後に回復見込みリトライ可(指定秒だけ待つ)
QuotaFailure が分単位クォータRPM/TPM 超過。すぐ回復リトライ可(バックオフ)
自前の月次支出ゲートが上限超過今月の予算を使い切った可能性大リトライ不可(縮退へ)
RetryInfo なし・原因不明の枯渇が連続判別不能だが回復していない保守的に遮断(ブレーカーを開く)

ここでの設計判断は「迷ったら叩かない」です。リトライして失われるのは時間とわずかなレイテンシ予算ですが、枯渇しているプロジェクトを叩き続けて得られるものは何もありません。

分類層を実装する

Gemini の Python SDK(google-genai)でエラーを受け、上記の信号を読み取る分類器を組みます。SDK のバージョンによって例外の属性名は揺れるため、特定の属性に依存せず、防御的に取り出すのがコツです。

# pip install google-genai
from dataclasses import dataclass
from enum import Enum
import json
import re
 
 
class Verdict(Enum):
    RETRYABLE = "retryable"        # 待てば直る(バックオフ可)
    TERMINAL = "terminal"          # 今月は無駄(縮退へ)
    UNKNOWN = "unknown"            # 判別不能(保守的に遮断)
 
 
@dataclass
class Classification:
    verdict: Verdict
    retry_after_s: float | None    # サーバー指定の待機秒(あれば)
    reason: str
 
 
def _extract_details(err) -> dict:
    """例外から構造化詳細を防御的に取り出す。SDK 差異を吸収する。"""
    # google-genai の APIError は .code / .status / .details を持つことが多いが、
    # バージョン差があるため getattr と文字列フォールバックで拾う。
    payload = {}
    for attr in ("details", "response_json", "args"):
        val = getattr(err, attr, None)
        if isinstance(val, dict):
            payload = val
            break
        if isinstance(val, (list, tuple)) and val and isinstance(val[0], dict):
            payload = val[0]
            break
    if not payload:
        # 最後の手段:文字列化した本文から JSON 片を拾う
        text = str(getattr(err, "message", "") or err)
        m = re.search(r"\{.*\}", text, re.DOTALL)
        if m:
            try:
                payload = json.loads(m.group(0))
            except json.JSONDecodeError:
                payload = {}
    return payload
 
 
def _retry_delay_seconds(details: dict) -> float | None:
    """google.rpc.RetryInfo の retryDelay(例 "5s")を秒に変換する。"""
    error = details.get("error", details)
    for d in error.get("details", []):
        t = d.get("@type", "")
        if "RetryInfo" in t:
            raw = d.get("retryDelay", "")
            m = re.match(r"(\d+(?:\.\d+)?)s", str(raw))
            if m:
                return float(m.group(1))
    return None
 
 
def _quota_dimension(details: dict) -> str | None:
    """QuotaFailure からクォータ ID(分単位か否かの手がかり)を取り出す。"""
    error = details.get("error", details)
    for d in error.get("details", []):
        if "QuotaFailure" in d.get("@type", ""):
            for v in d.get("violations", []):
                qid = v.get("quotaId") or v.get("subject") or ""
                if qid:
                    return qid
    return None
 
 
def classify_429(err, monthly_budget_exhausted: bool) -> Classification:
    """429 を3値に分類する。monthly_budget_exhausted は自前の支出ゲート由来。"""
    details = _extract_details(err)
    delay = _retry_delay_seconds(details)
    qid = _quota_dimension(details) or ""
 
    # 自前ゲートが「今月もう無理」と言っているなら、それを最優先で信じる
    if monthly_budget_exhausted:
        return Classification(Verdict.TERMINAL, None, "monthly spend gate exhausted")
 
    # サーバーが待機秒を指定 → レート制限。素直に待つ
    if delay is not None:
        return Classification(Verdict.RETRYABLE, delay, f"server RetryInfo={delay}s")
 
    # 分単位クォータ(PerMinute 等)に当たっている → 待てば回復
    if re.search(r"(per[-_ ]?minute|PerMinute|RPM|TPM)", qid, re.IGNORECASE):
        return Classification(Verdict.RETRYABLE, None, f"per-minute quota: {qid}")
 
    # 日次・月次・プロジェクトのクォータ枯渇 → 待っても基本直らない
    if re.search(r"(per[-_ ]?day|PerDay|monthly|project)", qid, re.IGNORECASE):
        return Classification(Verdict.TERMINAL, None, f"long-window quota: {qid}")
 
    # RetryInfo もクォータ次元も読めない枯渇 → 判別不能。保守的に扱う
    return Classification(Verdict.UNKNOWN, None, "no RetryInfo, unknown quota")

ポイントは、monthly_budget_exhausted という自前の真偽値を最優先で信用していることです。なぜなら、これは推測ではなく自分の手元の記録に基づく事実だからです。API のエラー形状は将来変わり得ますが、「今月の概算支出が上限に達した」という判定は自分のコードが握っています。Spend Cap 時代の堅牢さは、ここをサーバー任せにしないことから来ます。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
429 を浴びるたびに指数バックオフで叩き続けていた人が、リトライ可・不可を機械的に判定する分類層を今日から実装できる
Project Spend Caps で月額上限に当たった時に、無駄なリトライでレイテンシだけ膨らませず、キャッシュや軽量モデルへ静かに縮退させる回路を入手できる
RetryInfo・QuotaFailure・自前の月次予算ゲートという3つの信号をどう組み合わせて『叩いてよいか』を決めるか、判断基準を持ち帰れる
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Gemini Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

API / SDK2026-04-11
Gemini API レート制限と 429 対策の運用ノート ― 個人開発アプリで踏んだ落とし穴と対処
Gemini API のレート制限と 429 エラーに本番アプリで遭遇した経験から、指数バックオフ・アダプティブ制御・マルチキープール・Cloud Monitoring 連携を実装メモとしてまとめました。
API / SDK2026-06-26
Gemini API のセーフティフィルタが正当な応答を黙って落とすとき — 全切りせず誤検知だけを救う運用メモ
本番のGemini APIで正当なプロンプトがSAFETYでブロックされる誤検知を、全カテゴリ無効化に逃げずに扱う運用メモ。入力ブロックと出力ブロックの切り分け、誤検知率の計装、カテゴリ別の段階的リカバリまでを実装で整理します。
API / SDK2026-06-25
Gemini API × TypeScript 型安全AIアプリケーション設計ガイド — Zodスキーマ・Structured Output・ストリーミングの統合アーキテクチャ
Gemini APIとTypeScriptで型安全なAIアプリケーションを構築する方法を解説。Zodバリデーション、Structured Output、ストリーミング、エラーハンドリングを統合した本番アーキテクチャを実装します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →