API 開発で最初に壁にぶつかるのが、レート制限エラーです。
Gemini API を使い始めてしばらく経つと、429 Too Many Requests や RESOURCE_EXHAUSTED といったエラーに遭遇することがあります。これらは「使いすぎ」のシグナルですが、ただ待てばいいというわけでもなく、適切な実装がないとエラーが繰り返し発生します。
ここではGemini API のレート制限の仕組みを理解した上で、実際のコードで対策する方法を整理しました。
Gemini API のレート制限の種類
Gemini API には、主に3種類のレート制限があります。
RPM(Requests Per Minute): 1分あたりのリクエスト数の上限 TPM(Tokens Per Minute): 1分あたりに処理できるトークン数の上限 RPD(Requests Per Day): 1日あたりのリクエスト数の上限
無料枠(Google AI Studio や Gemini API の無料ティア)では特にこれらの制限が厳しく、開発中にすぐ上限に到達することがあります。有料の API を使用している場合も、急激なトラフィック増加時にはレート制限に当たることがあります。
最新の制限値は Google の公式ドキュメントで確認してください。モデルや利用プランによって異なります。
基本的な解決策:指数バックオフの実装
レート制限エラーが発生したとき、最も重要な対策は「適切な待機とリトライ」です。単純に固定時間待機するのではなく、リトライのたびに待機時間を増やす「指数バックオフ」が標準的な実装パターンです。
import time
import random
import google.generativeai as genai
from google.api_core import exceptions
genai.configure(api_key="YOUR_GEMINI_API_KEY")
model = genai.GenerativeModel("gemini-2.5-flash-preview-05-20")
def generate_with_retry(prompt: str, max_retries: int = 5) -> str:
"""
指数バックオフでレート制限エラーを自動リトライする関数
"""
for attempt in range(max_retries):
try:
response = model.generate_content(prompt)
return response.text
except exceptions.ResourceExhausted as e:
if attempt == max_retries - 1:
# 最終リトライでも失敗した場合は例外を再送出
raise
# 指数バックオフ: 2^attempt 秒 + ランダムジッター
wait_time = (2 ** attempt) + random.uniform(0, 1)
print(f"レート制限エラー。{wait_time:.1f}秒後にリトライ... (試行 {attempt + 1}/{max_retries})")
time.sleep(wait_time)
except Exception as e:
# レート制限以外のエラーはリトライしない
raise
# 使用例
try:
result = generate_with_retry("Pythonの非同期処理について説明してください")
print(result)
except exceptions.ResourceExhausted:
print("最大リトライ回数に達しました。しばらく待ってから再実行してください。")random.uniform(0, 1) のジッター(ゆらぎ)を加えているのは、複数のクライアントが同時にリトライするときに「全員が同じタイミングでリクエストを送る」状況(雷鳴群れ問題)を防ぐためです。
バッチ処理でのレート制限対策
複数のリクエストを一度に処理するバッチ処理では、事前にリクエスト間隔を制御する点が肝心です。
import time
import asyncio
import google.generativeai as genai
from google.api_core import exceptions
genai.configure(api_key="YOUR_GEMINI_API_KEY")
model = genai.GenerativeModel("gemini-2.5-flash-preview-05-20")
async def process_batch(prompts: list[str], rpm_limit: int = 10) -> list[str]:
"""
RPM制限を考慮してバッチ処理を行う非同期関数
rpm_limit: 1分あたりの最大リクエスト数
"""
results = []
delay = 60.0 / rpm_limit # リクエスト間の最小待機時間(秒)
for i, prompt in enumerate(prompts):
try:
response = model.generate_content(prompt)
results.append(response.text)
# 最後のリクエスト以外は待機
if i < len(prompts) - 1:
await asyncio.sleep(delay)
except exceptions.ResourceExhausted:
# エラーが発生した場合は追加で30秒待機してリトライ
print(f"レート制限。30秒待機してリトライします...")
await asyncio.sleep(30)
try:
response = model.generate_content(prompt)
results.append(response.text)
except Exception as e:
results.append(f"エラー: {str(e)}")
return results
# 使用例
async def main():
prompts = [
"Pythonのリスト内包表記を説明してください",
"辞書の使い方を教えてください",
"クラスの基本的な書き方を示してください",
]
results = await process_batch(prompts, rpm_limit=10)
for i, result in enumerate(results):
print(f"--- 結果 {i+1} ---")
print(result[:200])
asyncio.run(main())よくある間違いと正しい対処法
間違い1: エラーをキャッチせずにループを続ける
# ❌ レート制限エラーでループが止まる
for prompt in large_list:
response = model.generate_content(prompt) # エラーで全体が停止
results.append(response.text)# ✅ エラーをキャッチして適切に処理する
for prompt in large_list:
try:
response = model.generate_content(prompt)
results.append(response.text)
except exceptions.ResourceExhausted:
time.sleep(60) # 1分待機してリトライ
response = model.generate_content(prompt)
results.append(response.text)間違い2: 大きなプロンプトを分割せずに送る
トークン数の多いリクエストは TPM 制限に当たりやすくなります。長い文書を一度に処理しようとするより、チャンクに分割して複数のリクエストに分けることで、TPM の消費を分散できます。
間違い3: エラーメッセージを確認しない
ResourceExhausted エラーにはエラーメッセージが含まれており、どの制限(RPM か TPM か RPD か)に当たっているかが分かる場合があります。エラーメッセージを確認することで、適切な対策が取りやすくなります。
無料枠での開発を効率化するコツ
開発初期に無料枠を使っている場合は、以下の工夫でレート制限エラーを減らせます。
リクエストをキャッシュします。同じプロンプトに対して毎回 API を叩くのではなく、初回のレスポンスを保存しておいて再利用します。特に開発中のテストや反復作業で効果があります。
短いプロンプトで動作確認します。本番用の長いプロンプトの開発中は、まず短い要約プロンプトで動作確認し、完成したら本番プロンプトで検証します。
開発と本番で API キーを分ける。本番の APIキーに開発のリクエストが混ざると、制限管理が難しくなります。
Gemini API の活用全般については、Google AI Studio と OpenAI Playground の比較記事も参考になります。
送信前にトークン数を数えて TPM 超過を防ぐ
レート制限のうち TPM(1分あたりのトークン数)は、リクエストの本数が少なくても引っかかります。長い文書を1回で送ると、それだけで上限に届いてしまうことがあるためです。
送信前にトークン数を数えておくと、TPM 超過を未然に避けられます。count_tokens は API を呼びますが、生成の課金は発生しません。検証用に気軽に使える点がありがたいところです。
def count_tokens_before_send(model_name: str, contents: list) -> int:
"""送信前にトークン数を数えて確認します(生成課金は発生しません)"""
model = genai.GenerativeModel(model_name)
token_count = model.count_tokens(contents)
total = token_count.total_tokens
# 警告閾値(例:50万トークン)
if total > 500_000:
print(f"⚠️ 大きなリクエスト: {total:,}トークン")
print(" 処理に時間がかかる可能性があります")
return total
token_count = count_tokens_before_send(
"gemini-2.5-pro-latest",
["非常に長い文書の内容..."]
)
print(f"トークン数: {token_count:,}")同じプロンプトでも入力トークンが10万から50万に増えると、レイテンシが2〜3倍に伸びることがあります。「入れられること」と「入れるべきこと」は別なのだと、計測しながら実感しました。
エラーコード別に「待つべきか、直すべきか」を見分ける
Gemini API が返すエラーを、すべて同じように扱ってはいけません。リトライで解決するエラーと、コードを直さないと永遠に直らないエラーがあるためです。形式の間違ったリクエストをリトライしても、クォータを無駄に消費するだけになります。
リトライすべきエラーは、サーバー側や混雑が原因の一過性のものです。
429 RESOURCE_EXHAUSTED:レート制限。指数バックオフでリトライします500 INTERNAL_SERVER_ERROR:一時的なサーバー問題。少し待ってリトライします503 SERVICE_UNAVAILABLE:サービス一時停止。数分待ってリトライします
リトライしてはいけないのは、リクエスト側に原因があるエラーです。
400 INVALID_ARGUMENT:リクエストの形式が間違っています。コードを修正します403 PERMISSION_DENIED:API キーの権限が不足しています404 NOT_FOUND:モデル名が間違っていますStopCandidateException(安全フィルター):プロンプト自体を見直す必要があります
def classify_error(error: Exception) -> str:
"""エラーの種類を分類します"""
error_str = str(error).lower()
if "429" in error_str or "resource_exhausted" in error_str:
return "rate_limit" # リトライ可
elif "500" in error_str or "503" in error_str:
return "server_error" # リトライ可
elif "400" in error_str:
return "invalid_request" # コード修正が必要
elif "403" in error_str:
return "auth_error" # API キーを確認
else:
return "unknown"最初にこの分類を1か所にまとめておくと、リトライ処理の見通しがよくなります。
使用量をモニタリングしてコストを把握する
レート制限への対処と並んで、本番で効いてくるのが使用量の可視化です。どれだけ呼び、どれだけトークンを使ったかが見えていないと、コストもエラーの傾向もつかめません。
import time
from dataclasses import dataclass, field
@dataclass
class UsageStats:
"""API の使用量を追跡するシンプルなクラスです"""
requests: int = 0
input_tokens: int = 0
output_tokens: int = 0
errors: int = 0
start_time: float = field(default_factory=time.time)
def add_response(self, response: genai.types.GenerateContentResponse):
self.requests += 1
if hasattr(response, "usage_metadata"):
self.input_tokens += response.usage_metadata.prompt_token_count
self.output_tokens += response.usage_metadata.candidates_token_count
def print_summary(self):
elapsed = time.time() - self.start_time
# Gemini 2.5 Pro の料金(2026年4月時点・最新値は公式で確認してください)
# 入力: $1.25/1M tokens(200K以下)、出力: $10/1M tokens
input_cost = (self.input_tokens / 1_000_000) * 1.25
output_cost = (self.output_tokens / 1_000_000) * 10.0
print(f"=== 使用量サマリー ===")
print(f"経過時間: {elapsed:.1f}秒")
print(f"リクエスト数: {self.requests}件(エラー: {self.errors}件)")
print(f"入力トークン: {self.input_tokens:,}")
print(f"出力トークン: {self.output_tokens:,}")
print(f"推定コスト: ${input_cost + output_cost:.4f}")
stats = UsageStats()
response = generate_with_retry("質問内容")
stats.add_response(response) if response else None
stats.print_summary()料金は改定されることがあるため、係数は公式の料金ページで確認してから使ってください。
本番環境で見落としがちな設定 — タイムアウトとフォールバック
レート制限の実装が整っても、本番ではもう一歩の備えが要ります。
タイムアウトを明示します。既定では長時間かかるリクエストがタイムアウトしないため、本番ではハングしたリクエストがリソースを占有し続けます。
response = model.generate_content(
contents,
request_options={"timeout": 120} # 120秒でタイムアウト
)モデルのフォールバックを用意します。混雑時に Gemini 2.5 Pro が詰まっても、一部のタスクは Flash で十分な品質を保てます。重要度でモデルを振り分けておくと、コストと Pro 側のレート制限への露出を同時に下げられます。
最初はとにかくログを残します。どのエラーが何回起きているかが見えれば、対処の優先順位は自然に決まっていきます。本番のエラー分布は事前の予想とずれることが多く、見えないものは直しようがありません。
レート制限は「壊れている」サインではありません
レート制限エラーは、Gemini API が壊れているサインではありません。適切なリトライ実装とリクエスト間隔の管理さえあれば、本番環境でも安定して動くシステムが作れます。まず自分のアプリケーションの RPM/TPM の使用量を把握し、制限に余裕を持たせた設計にしておくことが、長く安定して動かす近道です。
私自身、個人開発で Dolice Labs の複数ブログの記事生成や、手元のアプリのバックエンドを Gemini で回しています。最初の頃はバッチをそのまま流して 429 に何度もぶつかり、そのたびに手が止まりました。間隔を空けてリトライを挟むだけで、同じ処理が静かに通るようになった瞬間に、レート制限は敵ではなく前提なのだと腑に落ちました。
エラーログを丁寧に取りながら、自分のトラフィックで何が起きるかを少しずつ確かめていく。その積み重ねが、結局いちばん堅い対策になります。この記事が参考になれば幸いです。