Gemini APIの503(high demand)が続くときの対処:google-genaiは設定しないと再試行しない。回数の決め方と、逃がし先に3.5 Flashを選ばない理由

当ページのリンクには広告が含まれています。

Gemini APIで503 UNAVAILABLE(「This model is currently experiencing high demand.」)が返り、処理が最後まで終わらない。そんな報告が10月下旬から続いています。対象はおもにgemini-3.8-flashで、Googleの開発者フォーラムには11月11日にも「3.8 Flashも3.6 Flashも503ばかり」という投稿がありました。

先に結論です。

  • PythonのSDK(google-genai)は、設定しないと503を1回で諦めます。再試行させるにはHttpRetryOptionsを明示します。
  • 再試行は3回・十数秒までに絞ります。フォーラムの報告では503が数日単位で続いており、待っても直らない時間帯があります。
  • それでも503なら別のモデルへ逃がします。ただしgemini-3.5-flashとgemini-3.7-flashは、送っても別のモデル(3.6 Flashと3.8 Flash)へ自動で転送されます。逃がし先には入れません。

今回は、google-genai 3.3.0の再試行の挙動を、手元に立てた擬似サーバーで数えました。本物のGemini APIには送っていないので、503がいま何割の確率で返るかは測れていません。その点は最後にまとめます。

目次

503は「待てば直る」とは限らない

Googleのエラー文書は、503を「一時的な過負荷または停止」と説明し、指数バックオフ(間隔を倍々に広げる再試行)を勧めています(Gemini APIのエラーコード一覧)。Googleが2月にフォーラムへ固定した投稿でも、503は利用枠とは無関係なサーバー側の過負荷だとして、再試行の設定と、急がない処理のBatch APIへの移行を勧めていました。

一方、利用者の報告を時系列で並べると、数分の再試行で片付く話ではなさそうです(Google AI Developers Forumのスレッド)。

  • 10月22日・27日:無料枠で3.8 Flashの503が続くという投稿と、「AI Studioの画面では動くのにAPIだけ503」という投稿。
  • 11月3日:有料のStandard tierの利用者が、11月2日から3.8 Flashで503が続くと報告。ステータスページは「Operational(正常)」のままでした。
  • 11月11日:「約1か月続いている。3.8 Flashと3.6 Flashは常に503で、APIで動くのは3.5 Flash-Liteだけ」という投稿。1人の報告です。

Googleは原因や収束の見込みを説明していません。ですから、再試行は「短い混雑をやり過ごす」ためのものと割り切り、長引いたときの逃がし先を別に用意しておくのが現実的です。

google-genaiは設定しないと再試行しない

google-genai 3.3.0のソース(_api_client.py)を読むと、retry_optionsを渡さないクライアントは「1回で終了」に設定されています。HttpRetryOptions()を渡すと、初回を含め最大5回送ります。待ち時間は1秒から始めて倍々に増え、上限は60秒です。再試行の対象は、状態コード408・429・500・502・503・504と、通信のタイムアウトです。

本当にその通り動くかを、ローカルの擬似サーバーで数えました。サーバーはモデル名ごとに決めたエラーを返し、届いたリクエストの時刻を記録します。結果は次のとおりです。

  • 設定なしで503:届いたのは1回。すぐにServerError 503 UNAVAILABLEで止まりました。
  • HttpRetryOptions()(値はすべて既定)で503:5回届き、間隔は1.7秒・2.3秒・4.3秒・8.2秒。失敗が確定するまで合計16.5秒かかりました。
  • attempts=3, initial_delay=2で503:3回で、間隔は2.2秒・4.8秒、合計7.0秒。
  • 503にRetry-After: 10(10秒後に再送せよ)を付けた場合:2回目は1.1秒後に届きました。SDKはRetry-Afterを見ていません。
  • 429 RESOURCE_EXHAUSTED:既定の対象に含まれるので3回再送されました。http_status_codes=[503]のように対象から外すと1回で止まります。
  • 404 NOT_FOUND:再試行の対象外で、1回で止まりました。
  • 応答に5秒かかる状態でtimeout=2000(ミリ秒):設定なしでは2.0秒でReadTimeout。attempts=2ではタイムアウトも再試行され、合計5.6秒でした。

注意したいのは429です。エラー文書では、429には1分あたりの上限に当たった場合と、1日の上限を使い切った場合があり、後者は「リセットを待つ」が対処です。SDKは状態コードだけで判断するので、1日の上限切れでも数十秒かけて再送を繰り返します。分あたりの上限によく当たる処理でなければ、429は対象から外しておくほうが、失敗に早く気づけます。

再試行の設定例

夜間のバッチのように、失敗したら後でやり直せる処理を想定した設定です。1回の呼び出しが待つのは最大で十数秒です。

from google import genai
from google.genai import types

retry = types.HttpRetryOptions(
    attempts=3,                              # 初回を含めて3回
    initial_delay=2,                         # 2秒・4秒と間隔を広げる(+最大1秒の揺らぎ)
    max_delay=10,
    http_status_codes=[500, 502, 503, 504],  # 429(上限超過)は再試行しない
)
client = genai.Client(http_options=types.HttpOptions(retry_options=retry, timeout=60_000))  # timeoutはミリ秒

timeoutを付けない場合、SDKは応答を待つ時間に上限を設けません(ソースで確認)。フォーラムには「90秒待ってタイムアウトした」という報告もあります。画面の操作に応答する処理なら、ここを短めにして、再試行の回数も減らします。

逃がし先のモデルは「転送先」で選ぶ

Qiitaには「3.8 Flashから3.5 Flashに替えたら直った」という11月12日の記事があります。ところが、Googleのモデルの提供終了の一覧によると、gemini-3.5-flashはAI Studioから外れた後もエンドポイントが残っていて、APIへのリクエストはgemini-3.6-flashへ自動転送されます。つまり実際に応答しているのは3.6 Flashです。gemini-3.7-flashはもっと注意が要ります。転送先がgemini-3.8-flashなので、503の出ている3.8 Flashから3.7 Flashへ替えても、同じモデルに送り直すだけです。

同じ一覧で、転送ではなく単独で提供されているFlash系は次の4つです(2026年11月14日に確認)。

  • gemini-3.6-flash:7月21日公開。停止日は未発表。ただし11月11日の投稿では、これも503が多いとされています。
  • gemini-3.5-flash-lite:7月21日公開。停止日は未発表。同じ投稿で「動いた」とされたモデルです。軽量版なので、出力の質は自分の処理で確かめてから切り替えます。
  • gemini-4-hydrogen:11月12日公開。単価は3.8 Flashの2倍です(1月1日に3.8 Flashが値上げされた後は同額)。乗り換えの費用はGemini 4 Hydrogenと3.8 Flashの損益分岐の試算で計算しています。
  • gemini-3.1-flash-lite:2027年5月7日以降に停止の可能性があると予告されています。長く使う逃がし先には向きません。

どのモデルにいま空きがあるかは、時間帯で変わるはずです。順番を固定するより、上から試して5xxなら次へ進む作りにしておくと、モデルの入れ替えも1行で済みます。

from google.genai import errors

# 上から順に試す。gemini-3.5-flash と gemini-3.7-flash は転送先が別モデルなので入れない
MODELS = ["gemini-3.8-flash", "gemini-3.6-flash", "gemini-3.5-flash-lite"]


def generate(client, contents):
    for model in MODELS:
        try:
            return client.models.generate_content(model=model, contents=contents)
        except errors.ServerError as error:  # 5xx。再試行(上の設定)を使い切った後に来る
            print(f"{model}: {error.code} {error.status}。次のモデルへ")
    raise RuntimeError("すべてのモデルが5xxを返しました")

擬似サーバーでは、1つ目のモデルが常に503を返す状態で、この関数をattempts=2と組み合わせて動かしました。1つ目に2回送った後、2つ目のモデルの応答を返しました。

自分の環境の問題か、先に切り分ける

503がGoogle側の混雑なのか、自分のコードの問題なのかは、SDKを通さずに1回送ると分かります。APIキーはURLの?key=ではなくヘッダーで渡すと、ログやシェルの履歴にURLごと残りません。

curl -s -o /dev/null -w "%{http_code}\n" \
  -H "x-goog-api-key: $GEMINI_API_KEY" -H "Content-Type: application/json" \
  -d '{"contents":[{"parts":[{"text":"hi"}]}]}' \
  https://generativelanguage.googleapis.com/v1beta/models/gemini-3.8-flash:generateContent
  • 503:Google側の混雑です。再試行と逃がし先で対処します。
  • 404:モデル名の誤りか、提供が終わったモデルです。再試行では直りません。
  • 429:分あたりの上限か、1日の上限です。返ってきたエラー本文で区別します。
  • 200:APIは応答しています。SDKの設定やプロンプトの大きさを確かめます。

急がない処理なら、Googleが固定投稿で勧めていたBatch APIへ移す方法もあります。料金ページの単価は通常の半額です(11月13日に確認)。

確かめていないこと

  • 本物のAPIでの503の頻度:今回は自動実行の権限の都合で、Gemini APIへ試験送信していません。どのモデルにいま空きがあるかも未確認です。
  • 503が無料枠の1日の上限に数えられるか:フォーラムで指摘はありますが、Googleの説明はありません。
  • JavaScript版SDK(@google/genai)の既定値:Python版だけを確かめました。
  • 逃がし先のモデルの出力の質:3.5 Flash-Liteなどで、自分の処理の精度が保てるかは試していません。

検証に使ったスクリプト

uvx --with google-genai==3.3.0 python genai_retry.pyで動きます。送信先は127.0.0.1の擬似サーバーで、APIキーも架空の値なので、料金はかかりません。数値は実行のたびに揺らぎ(jitter)の分だけ変わります。

"""google-genai SDKが503などのエラーを何回・何秒間隔で再試行するかを、手元の擬似サーバーで数える。

使い方: uvx --with google-genai==3.3.0 python genai_retry.py
本物のGemini APIには接続しない(APIキーは架空の値、送信先は127.0.0.1)。
擬似サーバーは、モデル名ごとに決めたステータスとエラー本文を返し、受けたリクエストの時刻を記録する。
"""
import json
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

from google import genai
from google.genai import errors, types

PORT = 18765
OVERLOADED = (503, "UNAVAILABLE", "This model is currently experiencing high demand. "
              "Spikes in demand are usually temporary. Please try again later.")
QUOTA = (429, "RESOURCE_EXHAUSTED", "You exceeded your current quota, please check your plan and billing details.")
NOT_FOUND = (404, "NOT_FOUND", "models/old-model is not found for API version v1beta.")
# モデル名 → 返す応答。"ok"は正常応答、"slow"は5秒待ってから正常応答
BEHAVIOR = {"busy": OVERLOADED, "quota": QUOTA, "gone": NOT_FOUND, "ok": "ok", "slow": "slow",
            "busy-retry-after": OVERLOADED}
hits = []


class FakeGemini(BaseHTTPRequestHandler):
    def do_POST(self):
        self.rfile.read(int(self.headers.get("Content-Length", 0)))
        model = self.path.split("/models/")[1].split(":")[0]
        hits.append((model, time.monotonic()))
        behavior = BEHAVIOR[model]
        if behavior == "slow":
            time.sleep(5)
        if behavior in ("ok", "slow"):
            body = {"candidates": [{"content": {"role": "model", "parts": [{"text": f"answer from {model}"}]}}]}
            return self.reply(200, body)
        code, status, message = behavior
        extra = {"Retry-After": "10"} if model == "busy-retry-after" else {}
        self.reply(code, {"error": {"code": code, "message": message, "status": status}}, extra)

    def reply(self, code, body, extra=None):
        data = json.dumps(body).encode()
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(data)))
        for key, value in (extra or {}).items():
            self.send_header(key, value)
        self.end_headers()
        self.wfile.write(data)

    def log_message(self, *args):
        pass


def client(retry=None, timeout_ms=None):
    options = types.HttpOptions(base_url=f"http://127.0.0.1:{PORT}", retry_options=retry, timeout=timeout_ms)
    return genai.Client(api_key="dummy-key-for-local-test", http_options=options)


def run(label, model, retry=None, timeout_ms=None):
    """1回のgenerate_contentで、擬似サーバーが受けた回数・間隔・最後の結果を表示する。"""
    hits.clear()
    start = time.monotonic()
    try:
        sdk = client(retry, timeout_ms)  # 一時オブジェクトのままだと先に閉じられる
        result = "成功: " + sdk.models.generate_content(model=model, contents="hi").text
    except errors.APIError as error:
        result = f"{type(error).__name__} code={error.code} status={error.status}"
    except Exception as error:  # 通信のタイムアウトなどAPIError以外
        result = f"{type(error).__module__}.{type(error).__name__}"
    elapsed = time.monotonic() - start
    times = [t for _, t in hits]
    gaps = [f"{b - a:.1f}" for a, b in zip(times, times[1:])]
    print(f"## {label}\n  受信 {len(hits)}回 / 間隔(秒) {gaps or '-'} / 合計 {elapsed:.1f}秒 / {result}")


def with_fallback(models, retry):
    """先頭のモデルから順に試し、503が再試行後も続いたら次のモデルへ移る。"""
    for model in models:
        try:
            sdk = client(retry)
            return model, sdk.models.generate_content(model=model, contents="hi").text
        except errors.ServerError as error:
            print(f"  {model}: {error.code} {error.status} → 次のモデルへ")
    raise RuntimeError("全モデルが503")


def main():
    server = ThreadingHTTPServer(("127.0.0.1", PORT), FakeGemini)
    threading.Thread(target=server.serve_forever, daemon=True).start()
    print(f"google-genai {genai.__version__}")
    run("503・retry_options指定なし(既定)", "busy")
    run("503・HttpRetryOptions()(値はすべて既定)", "busy", types.HttpRetryOptions())
    run("503・attempts=3, initial_delay=2", "busy", types.HttpRetryOptions(attempts=3, initial_delay=2))
    run("503+Retry-After: 10・HttpRetryOptions(attempts=2)", "busy-retry-after",
        types.HttpRetryOptions(attempts=2))
    run("429 RESOURCE_EXHAUSTED・HttpRetryOptions(attempts=3)", "quota", types.HttpRetryOptions(attempts=3))
    run("429・http_status_codes=[503](429を外す)", "quota",
        types.HttpRetryOptions(attempts=3, http_status_codes=[503]))
    run("404・HttpRetryOptions(attempts=3)", "gone", types.HttpRetryOptions(attempts=3))
    run("応答5秒・timeout=2000ms・retry_options指定なし", "slow", timeout_ms=2000)
    run("応答5秒・timeout=2000ms・HttpRetryOptions(attempts=2)", "slow", types.HttpRetryOptions(attempts=2), 2000)
    print("## 予備モデルへの切り替え(attempts=2, initial_delay=0.5)")
    hits.clear()
    model, text = with_fallback(["busy", "ok"], types.HttpRetryOptions(attempts=2, initial_delay=0.5))
    print(f"  採用: {model} / {text} / 受信 {[m for m, _ in hits]}")
    server.shutdown()


if __name__ == "__main__":
    main()
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

IT業界で10年、ソフトウェア開発をしてきたエンジニアです。今もChatGPT、Codex、Claude Codeを中心に、開発の効率化や生産性向上にAIをフル活用しています。応用情報技術者。登壇や執筆もしています。このサイトでは、仕事でAIを使う立場から、料金や利用条件、使い方を調べてまとめています。

目次