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()
