個人開発のアプリに Gemini API を組み込んだ初日、私自身がいちばん時間を取られたのは、エラーコードそのものよりも「これは待てば直るのか、それともコードを直すべきなのか」という判断でした。429 が返ってきても、レート制限なのか認証なのか、レスポンスの status を読むまでは分かりません。
ここでは、Gemini API で遭遇するエラーを status と finish_reason で分類し、本番で止まらないように処理へ落とし込む方法を、実際に使っているコードとともに整理します。狙いはエラーの一覧をつくることではなく、出たときに迷わず手が動く状態をつくることです。
最初の90秒でやる切り分け
エラーが出たとき、私がまず足すのは対処ではなく1行の出力です。例外の型と、レスポンスに載っている status を先に見る。ここを飛ばして「とりあえずリトライ」から書き始めると、直らないものを延々と待ち続ける実装ができあがります。
except Exception as e:
# 型と code を最初に出す。ここが切り分けの起点になります
print ( type (e). __name__ , getattr (e, "code" , None ), getattr (e, "message" , e))
見るべき軸は2つだけです。
HTTP status(400 / 401 / 403 / 429 / 500・503)— そもそもリクエストが受理されたか
finish_reason(SAFETY / RECITATION / MAX_TOKENS)— 受理されたうえで、生成が途中で止まったか
前者は「送り方」の問題、後者は「中身」の問題です。同じ「応答が返ってこない」という症状でも、この2つは打つ手がまったく違います。以降は、この2軸に沿って status の系統ごとに整理していきます。
400 Bad Request — リクエスト形式・モデル名の誤り
400 Bad Requestエラーは、サーバーがリクエストを理解できない場合に発生します。主な原因は3つです。
1. モデル名の誤り
最も多いのはモデル名を間違えるケースです。Gemini APIで利用できるモデルは限られています。
import google.generativeai as genai
genai.configure( api_key = "YOUR_GEMINI_API_KEY" )
# ❌ 間違い:モデル名が存在しない
model = genai.GenerativeModel( "gemini-invalid-model" )
# ✅ 正解:実在するモデル名を使う
model = genai.GenerativeModel( "gemini-2.5-flash" )
response = model.generate_content( "こんにちは" )
print (response.text)
最新で利用可能なモデル一覧は、Google AI Studioの「モデルの選択」ページ で確認できます。よく使われるモデルは以下の通りです:
gemini-2.5-flash — 高速・低単価。実装中の試行錯誤はこれで十分です
gemini-2.5-pro — 精度重視。長文の要約や複雑な推論向け
gemini-3-pro — 最新世代。対応状況はリージョンによって差があります
2. JSONフォーマットが不正
TypeScript/JavaScriptでAPIを呼び出す場合、リクエストボディのJSON形式が正しくないと400エラーが発生します。
import { GoogleGenerativeAI } from "@google/generative-ai" ;
const genAI = new GoogleGenerativeAI ( "YOUR_GEMINI_API_KEY" );
const model = genAI. getGenerativeModel ({ model: "gemini-2.5-flash" });
// ✅ 正しいリクエスト形式
const response = await model. generateContent ({
contents: [
{
role: "user" ,
parts: [{ text: "こんにちは" }],
},
],
});
特にcontents、parts、roleなどの必須フィールドが欠落していないか確認してください。
3. 入力長が上限を超えている
テキスト入力が非常に長い場合、リクエストが400エラーで拒否されることがあります。Gemini APIのコンテキストウィンドウ(入力可能な最大文字数)は、モデルごとに異なります。
gemini-2.5-flash — 最大100万トークン
もし大量のテキストを処理する必要があれば、テキストを分割して複数回に分けてリクエストを送るようにしてください。
401/403 認証エラー — APIキーの設定方法
401(Unauthorized)または403(Forbidden)エラーは、APIキーが設定されていないか、権限がない場合に発生します。
Google AI Studioを使う場合
Google AI Studioはもっとも簡単にGemini APIを試す方法です。
Google AI Studio にアクセス
左側の「API キー」メニューから「新しいAPIキーを作成」をクリック
表示されたAPIキーをコピー
コード内で以下のように設定:
import google.generativeai as genai
# 方法1: 直接設定(開発用のみ推奨)
genai.configure( api_key = "YOUR_GEMINI_API_KEY" )
# 方法2: 環境変数から読み込み(本番環境推奨)
import os
api_key = os.getenv( "GEMINI_API_KEY" )
genai.configure( api_key = api_key)
環境変数に設定する場合は、Linux/macOSでは以下のコマンドを実行します:
export GEMINI_API_KEY = "your_actual_api_key"
Vertex AIを使う場合
Google Cloudプロジェクトを使用する場合は、Vertex AIを使う必要があります。Google AI Studioとは異なるセットアップが必要です。
import google.generativeai as genai
from google.auth import default
# GCPの認証を自動取得
credentials, project_id = default()
genai.configure( credentials = credentials)
Vertex AIを初めて使う場合は、GCPプロジェクトでVertex AI APIを有効にする必要があります。詳しくはVertex AIのドキュメント をご参照ください。
429 Too Many Requests — レート制限の理解と回避策
429エラーは、短時間に大量のリクエストを送った場合に発生します。Gemini APIには、無料枠と有料枠でそれぞれ異なるレート制限があります。
無料枠のレート制限
Google AI Studioで無料でGemini APIを使う場合、以下の制限があります:
1分あたり最大60リクエスト
1日あたりの総リクエスト数に上限あり
有料枠のレート制限
Google Cloudで有料契約した場合:
1分あたり最大360リクエスト(通常)
利用プランやモデルによって異なる
指数バックオフでの対処
429エラーが発生した場合、すぐにリトライするのではなく、待機時間を段階的に増やして再試行する「指数バックオフ」を使うのが定石です。
import time
import google.generativeai as genai
genai.configure( api_key = "YOUR_GEMINI_API_KEY" )
model = genai.GenerativeModel( "gemini-2.5-flash" )
def generate_with_retry (prompt, max_retries = 5 ):
"""指数バックオフを使ったリトライロジック"""
for attempt in range (max_retries):
try :
response = model.generate_content(prompt)
return response.text
except Exception as e:
if "429" in str (e):
# 待機時間: 1秒、2秒、4秒、8秒、16秒...
wait_time = 2 ** attempt
print ( f "レート制限に達しました。 { wait_time } 秒待機します..." )
time.sleep(wait_time)
else :
raise
# 使用例
result = generate_with_retry( "日本の首都は?" )
print (result)
TypeScript/JavaScriptの場合:
import { GoogleGenerativeAI } from "@google/generative-ai" ;
const genAI = new GoogleGenerativeAI ( "YOUR_GEMINI_API_KEY" );
const model = genAI. getGenerativeModel ({ model: "gemini-2.5-flash" });
async function generateWithRetry ( prompt : string , maxRetries = 5 ) : Promise < string > {
for ( let attempt = 0 ; attempt < maxRetries; attempt ++ ) {
try {
const response = await model. generateContent (prompt);
return response.response. text ();
} catch (error) {
if (error instanceof Error && error.message. includes ( "429" )) {
const waitTime = Math. pow ( 2 , attempt) * 1000 ; // ミリ秒に変換
console. log ( `レート制限に達しました。${ waitTime / 1000 }秒待機します...` );
await new Promise (( resolve ) => setTimeout (resolve, waitTime));
} else {
throw error;
}
}
}
throw new Error ( "最大リトライ回数に達しました" );
}
// 使用例
const result = await generateWithRetry ( "日本の首都は?" );
console. log (result);
どの上限に当たっているのか — RPM・RPD・TPM
429 は単一の原因ではありません。Gemini API では3種類の上限が独立して働いています。
RPM(Requests Per Minute)— 1分あたりのリクエスト数
RPD(Requests Per Day)— 1日あたりのリクエスト数
TPM(Tokens Per Minute)— 1分あたりに処理できるトークン数
個人開発のアプリで私自身が最初に引っかかったのは TPM でした。リクエスト数は少なくても、長いプロンプトを連続して送ると、件数の上限より先にトークンの上限へ達します。まずはどの上限に当たっているのかを切り分けることが、対処の出発点になります。
RESOURCE_EXHAUSTED は本当にクォータ超過か
429 のレスポンス本文には、次のように RESOURCE_EXHAUSTED が入ります。
{
"error" : {
"code" : 429 ,
"message" : "Resource has been exhausted (e.g. check quota)." ,
"status" : "RESOURCE_EXHAUSTED"
}
}
RESOURCE_EXHAUSTED はクォータ超過のサインで、時間をおけば回復します。一方で INVALID_ARGUMENT や PERMISSION_DENIED はリクエスト形式や認証の問題です。待っても直りませんので、status を確認して「待つべきか、直すべきか」を最初に見極めてください。
無料枠と有料枠でクォータはどれくらい違うか
無料枠は開発と検証のための枠で、本番運用は想定されていません。おおよその目安は次のとおりです(具体的な数値はモデルやリージョン、時期で変わりますので、最新値は公式ドキュメントでご確認ください)。
無料枠 — RPM はおよそ数十件/分、RPD は1日あたり数千件、TPM は1万トークン前後、同時接続は2程度
有料枠(従量課金)— RPM は数千件/分まで、RPD は実質的な上限なし、TPM は10万〜100万トークン、同時接続は100程度
見落としやすいのは、これらの上限がユーザー単位ではなくプロジェクト単位で共有される点です。同じプロジェクトで検証用スクリプトと本番アプリが同時に API を叩くと、合算で上限に達します。私自身、テスト用のバッチを流したまま本番アプリが 429 になり、原因の特定に時間を取られたことがありました。
規模の目安としては、月1万リクエスト未満なら無料枠と上限設定で十分、月100万リクエストまでは従量課金、それ以上は Google Cloud のボリューム相談が現実的です。
クォータの確認と引き上げ
現在の使用状況は、Google Cloud Console から確認できます。
Google AI Studio で対象のプロジェクトを確認します
Google Cloud Console で「API とサービス」から Generative Language API を開き、「クォータと使用」を確認します
「請求」で支払い情報を設定すると、有料枠の上限へ自動的に切り替わります(反映まで数分〜数時間かかることがあります)
予期しない請求を防ぐ
有料枠に切り替える前に、予算アラートを設定しておくと安心です。Google Cloud Console の「請求」から「予算」を開いて月額上限を決め、「予算の100%で通知」などのアラートを有効にしておきます。上限到達時に API 呼び出しを止める運用にしておけば、想定外の高額請求を避けられます。
使用率を監視する
本番では、上限に達してから気づくのでは手遅れです。Cloud Monitoring で quota_used_count を監視し、使用率が80%を超えたら通知するようにしておくと、有料枠への切り替えやリトライ設計を前もって判断できます。
displayName : Gemini API quota alert
conditions :
- displayName : RPM usage > 80%
conditionThreshold :
filter : |
resource.type="api"
AND metric.type="serviceruntime.googleapis.com/api/consumer/quota_used_count"
AND resource.service="generativelanguage.googleapis.com"
threshold_value : 0.8
comparison : COMPARISON_GT
500/503 サーバーエラー — Google側の一時的な障害
500(Internal Server Error)または503(Service Unavailable)エラーは、Google側のシステムに一時的な問題が発生している場合に起こります。これはあなたのコードやAPIキーの設定に問題があるわけではありません。
対応方法
少し待機してリトライする(指数バックオフを使用)
Google Cloud Status Dashboard(status.cloud.google.com )でGemini APIのステータスを確認
問題が継続する場合は、Google Cloud Supportに連絡する
import time
def generate_with_500_retry (prompt, max_retries = 3 ):
"""500/503エラーに対応したリトライロジック"""
for attempt in range (max_retries):
try :
response = model.generate_content(prompt)
return response.text
except Exception as e:
error_code = str (e)
if "500" in error_code or "503" in error_code:
wait_time = 5 * (attempt + 1 ) # 5秒、10秒、15秒
print ( f "サーバーエラーが発生しました。 { wait_time } 秒後に再試行します..." )
time.sleep(wait_time)
else :
raise
コンテキスト長の超過 — INVALID_ARGUMENT とトークン上限の対処
長いドキュメントや複数の画像をまとめて渡したとき、次のような400系エラーに出会うことがあります。
google.api_core.exceptions.InvalidArgument: 400 Request payload size exceeds the limit
マルチモーダルのリクエストでは、こう出ることもあります。
Invalid request: the total input token count exceeds the model's limit
入力がモデルのコンテキストウィンドウを超えると発生します。エラーを受けてから慌てるより、送信前にトークン数を数えておくと安全です。
import google.generativeai as genai
import os
genai.configure( api_key = os.environ[ "GOOGLE_API_KEY" ])
model = genai.GenerativeModel( "gemini-2.5-flash" )
def count_tokens (prompt: str ) -> int :
# 送信前にトークン数を確認する
return model.count_tokens(prompt).total_tokens
def safe_generate (prompt: str , max_chars: int = 30000 ) -> str :
if len (prompt) > max_chars:
# 末尾を切るより中間を省く方が、前後の文脈が保たれます
half = max_chars // 2
prompt = prompt[:half] + " \n ...[中略]... \n " + prompt[ - half:]
return model.generate_content(prompt).text
末尾だけを切り落とすと、肝心の結論が失われがちです。先頭と末尾を残して中間を省くと、指示と締めくくりの両方が手元に残ります。
長文を丸ごと扱いたいときは、Gemini 2.5 Pro や Flash の100万トークンコンテキストに切り替えるか、ドキュメントを意味のまとまりで分割して順に渡す方法を検討してみてください。
応答が空・ブロックされる — finish_reason で原因を切り分ける
エラーらしいエラーは出ないのに、テキストだけ取れない。これも実際によくつまずく場面です。
response = model.generate_content( "..." )
print (response.text) # AttributeError: 'NoneType' object has no attribute 'text'
response.text にいきなり触れず、まず candidates の有無と finish_reason を確認します。原因によって打つ手が変わるからです。
def safe_get_text (response) -> str | None :
# candidates が空なら入力段階でブロックされている
if not response.candidates:
print ( f "入力がブロックされました: { response.prompt_feedback } " )
return None
candidate = response.candidates[ 0 ]
reason = candidate.finish_reason.name
if reason == "SAFETY" :
print ( f "安全フィルターでブロック: { candidate.safety_ratings } " )
return None
if reason == "RECITATION" :
print ( "著作権コンテンツの検出によりブロック" )
return None
if reason == "MAX_TOKENS" :
# 途中までのテキストは取り出せる
print ( "max_output_tokens に達しました。上限を増やしてください" )
try :
return candidate.content.parts[ 0 ].text
except ( IndexError , AttributeError ):
return None
SAFETY ならプロンプトを中立的な表現に直します。RECITATION は既存の文章をそのまま再生成しようとしたときに出るので、要約や言い換えを求める形に変えます。MAX_TOKENS は max_output_tokens を引き上げます。原因ごとに対処が決まっていれば、空応答に振り回されずに済みます。
安全フィルターの感度は safety_settings で調整できますが、まずはプロンプト側の言い回しを見直すだけで解決することがほとんどです。入力プロンプトの内容を見直すところから始めてみてください。
すべての例外を1か所で分類する統一ハンドラ
エラーごとに try/except を書き散らすと、コードベースが大きくなるほど対処の抜け漏れが生まれます。私自身、復旧用のバッチを日常的に回すようになってからは、API 呼び出しを1つのラッパーに通し、status と finish_reason での分岐を一元化する形に落ち着きました。
考え方はシンプルです。例外を「待てば直る(リトライ)」「直さない限り直らない(即失敗)」「部分的に使える(警告して継続)」の3群に分けるだけです。
import time
import random
import google.generativeai as genai
from google.api_core import exceptions as gax
genai.configure( api_key = "YOUR_GEMINI_API_KEY" )
model = genai.GenerativeModel( "gemini-2.5-flash" )
# 待てば回復する見込みのある状態
RETRYABLE = (gax.ResourceExhausted, gax.ServiceUnavailable, gax.InternalServerError)
# 直さない限り回復しない状態
FATAL = (gax.InvalidArgument, gax.PermissionDenied, gax.Unauthenticated)
def call_gemini (prompt, max_retries = 5 , base = 1.0 ):
for attempt in range (max_retries):
try :
return model.generate_content(prompt)
except RETRYABLE as e:
# 指数バックオフ + ジッター(同時リトライの集中を避ける)
wait = base * ( 2 ** attempt) + random.uniform( 0 , base)
print ( f "[retry { attempt + 1 } ] { type (e). __name__ } : { wait :.1f } s 待機" )
time.sleep(wait)
except FATAL as e:
# ここで待つのは時間の無駄。呼び出し側へ即返す
raise RuntimeError ( f "修正が必要なエラー: { type (e). __name__ } : { e } " ) from e
raise RuntimeError ( "最大リトライ回数を超過しました" )
ジッター(random.uniform)を足しているのには理由があります。複数のワーカーが同時に 429 を受けると、固定の待機時間ではリトライのタイミングがそろってしまい、回復した瞬間にまた一斉にぶつかります。少しのゆらぎを入れるだけで、この再衝突が目に見えて減りました。
status と対処の対応表
迷ったときに立ち返れるよう、status と打つべき手を一枚にまとめておきます。
status / 症状 意味 打つ手 リトライ
RESOURCE_EXHAUSTED (429) RPM / RPD / TPM のいずれか超過 バックオフ + ジッター、上限の引き上げ する
INVALID_ARGUMENT (400) モデル名・JSON・入力長の誤り リクエストを修正する しない
UNAUTHENTICATED (401) APIキー未設定・無効 キーと環境変数を確認する しない
PERMISSION_DENIED (403) API未有効・権限不足 プロジェクト設定を確認する しない
INTERNAL / UNAVAILABLE (500/503) Google側の一時障害 待ってリトライ、ステータス確認 する
finish_reason=SAFETY 安全フィルターで遮断 プロンプトを中立な表現へ しない
finish_reason=MAX_TOKENS 出力上限に到達 max_output_tokens を引き上げ 条件付き
この表で効いてくるのは右端の「リトライ」列です。待っても無駄な FATAL 系をリトライし続けると、ユーザーを待たせたうえに結局失敗します。最初に「待つ価値があるか」を判定するだけで、体感のレスポンスが変わります。私はこの判定を、新しいプロジェクトを立ち上げるときも、いちばん最初に入れるようにしています。
google-genai SDK に移すと、例外の見え方が変わる
ここまでのコードは google-generativeai を前提にしています。後継の google-genai(from google import genai)へ移した場合、例外の階層そのものが変わりますので、統一ハンドラも合わせて書き換える必要があります。
import os
import time
import random
from google import genai
from google.genai import errors
client = genai.Client( api_key = os.environ[ "GEMINI_API_KEY" ])
def call_gemini (prompt, model = "gemini-2.5-flash" , max_retries = 5 , base = 1.0 ):
for attempt in range (max_retries):
try :
return client.models.generate_content( model = model, contents = prompt)
except errors.ServerError:
# 500 / 503 系。待てば回復する見込みがあります
time.sleep(base * ( 2 ** attempt) + random.uniform( 0 , base))
except errors.ClientError as e:
# 400 番台はまとめてここに来ます。code を見ないと 429 まで即失敗になります
if e.code == 429 :
time.sleep(base * ( 2 ** attempt) + random.uniform( 0 , base))
continue
raise RuntimeError ( f "修正が必要なエラー: { e.code } { e.message } " ) from e
raise RuntimeError ( "最大リトライ回数を超過しました" )
変わるのは判定の入口だけで、「待つか、直すか」という考え方はそのまま引き継げます。ServerError が旧 SDK の ServiceUnavailable / InternalServerError に、ClientError が InvalidArgument / PermissionDenied / ResourceExhausted にまとめて対応する、と読み替えてください。
移行直後にリトライが効かなくなったときは、まずこの e.code == 429 の分岐が抜けていないかを疑ってみてください。私は移行作業の初日にここで詰まり、レート制限のたびに即失敗する状態でしばらく気づかずにいました。例外の粒度が粗くなった分、code を明示的に見る責任がこちら側へ移っています。
モデル廃止とリージョン差を、運用側で吸収する
400 のなかでいちばん厄介なのは、書いた時点では正しかったモデル名が、時間の経過で無効になるケースです。リリース済みのアプリでこれが起きると、こちらは一行も変更していないのに、ある日を境に全ユーザーで落ちます。手元では再現しないぶん、原因にたどり着くまでが長くなります。
そこで私は、モデル名を単一の定数ではなく候補列として持たせています。
MODEL_CHAIN = ( "gemini-2.5-flash" , "gemini-2.5-pro" )
def generate_with_fallback (client, prompt):
last_error = None
for name in MODEL_CHAIN :
try :
return client.models.generate_content( model = name, contents = prompt)
except Exception as e:
text = str (e).lower()
# モデル名起因の 400 だけ次の候補へ回す
if "not found" in text or "is not supported" in text:
last_error = e
continue
raise
raise RuntimeError ( f "利用可能なモデルがありません: { last_error } " )
フォールバックの条件をモデル名起因のものだけに絞っているのが要点です。ここを「400 なら全部次へ」と緩くすると、入力長の超過のような本来直すべきエラーまで静かに次のモデルへ流れ、失敗の理由が見えなくなります。安全網は、広げすぎると診断能力を奪います。
リージョンについても考え方は同じです。Vertex AI 経由では、モデルの提供リージョンとプロジェクトのリージョンがずれていると、404 や PERMISSION_DENIED という一見無関係な顔で返ってきます。ローカルでは通るのに本番でだけ落ちる、という報告を受けたとき、私はまずリージョン指定を確認するようにしています。
構造化ログで「あとから追える」状態にしておく
エラーの切り分けが速いかどうかは、実のところ発生時の腕前よりも、事前にどんなログを残していたかで決まります。文字列を1行 print しておくだけでは、時間帯ごとの傾向も、どのモデルで多いのかも見えません。
呼び出しの結果を、辞書に詰めて JSON で吐く形にしておくと、あとから集計できます。
import json
import logging
import time
logger = logging.getLogger( "gemini" )
def log_call (model_name, started, response = None , error = None , attempt = 0 ):
record = {
"model" : model_name,
"elapsed_ms" : int ((time.monotonic() - started) * 1000 ),
"attempt" : attempt,
"status" : type (error). __name__ if error else "OK" ,
}
if response is not None and response.candidates:
record[ "finish_reason" ] = response.candidates[ 0 ].finish_reason.name
usage = getattr (response, "usage_metadata" , None )
if usage is not None :
record[ "prompt_tokens" ] = usage.prompt_token_count
record[ "output_tokens" ] = usage.candidates_token_count
if error is not None :
record[ "message" ] = str (error)[: 200 ]
# プロンプト本文は載せません(利用者の個人情報が混ざる可能性があります)
logger.info(json.dumps(record, ensure_ascii = False ))
finish_reason とトークン数を一緒に残しておくと、「エラーは出ていないのに出力が途中で切れている」という報告を、MAX_TOKENS だったのか SAFETY だったのかで即断できます。この2つは利用者から見た症状が同じなのに、対処は正反対です。
elapsed_ms を入れているのにも理由があります。500 が増える時間帯とレイテンシが伸びる時間帯が一致していれば Google 側の不調、一致していなければ自分のリトライ設計を疑う、という切り分けができるからです。プロンプト本文を残していない点も意図的で、問い合わせ内容がそのままログ基盤に流れ込む状態は、あとから消すのが本当に大変になります。
詰まったときに上から順に確認すること
手が止まったときのために、確認の順序を固定しておくと迷いが減ります。上ほど原因である頻度が高く、確認のコストが低い順に並べています。
例外の型と code を出力する(対処を書くのはこの後です)
API キーが環境変数に入っているか — echo $GEMINI_API_KEY で値の頭数文字だけ確認する
モデル名が現在も提供されているか — 古い記事から写した名前が生きているとは限りません
response.candidates が空でないかを見てから .text に触れる
429 なら RPM・RPD・TPM のどれに当たっているかを切り分ける(件数が少なくても TPM で止まります)
検証用のスクリプトが同じプロジェクトで動いていないか — クォータはプロジェクト単位で共有されます
私自身、6番目を見落として半日を溶かしたことがあります。本番アプリのコードをいくら読み返しても原因が出てこないときは、コードの外側を疑う番だという合図として使っています。
次の一歩
エラー対処の要点は、一覧を覚えることではなく、「出たときに status と finish_reason を見て、待つか直すかを即決できること」に尽きます。まずは今お使いのコードの API 呼び出しを、上で示した統一ハンドラ1つに通すところから始めてみてください。分岐が1か所にまとまっていれば、新しいエラーに出会ったときも対処の追加が一行で済みます。
困ったときの公式リソースも添えておきます。
同じところでつまずいた方の時間を、少しでも短くできれば嬉しいです。