This model is overloaded right now. Please try again later. is the error that greets many developers shortly after they start building with the Gemini API in earnest. The official documentation's advice — wait and retry — is technically accurate but doesn't tell you how to retry without making the problem worse.
Here's what's actually happening and what to do about it.
Two Kinds of "Overloaded"
The umbrella term covers two distinct situations with different solutions:
Model congestion (temporary): High-demand models like gemini-2.0-flash see traffic spikes at certain times, particularly during US business hours. Free-tier and low-cost models tend to be most affected. The fix is smart retrying.
Rate limit reached: Each API key has per-minute limits on requests (RPM) and tokens (TPM). Hit those limits and you get a 429 Resource Exhausted response that looks similar to an overload error. The fix is either slowing down your request rate or upgrading your quota.
You can distinguish them by HTTP status code:
503 Service Unavailable → Model overloaded (congestion)
429 Too Many Requests → Rate limit reached
408 / connection timeout → Network or processing timeout
The Right Retry Pattern: Exponential Backoff with Jitter
A fixed time.sleep(5) retry creates a thundering herd problem: if many clients hit the same error simultaneously and all retry at the same interval, the retry wave hits an already-congested API at the same moment. Exponential backoff spreads retries out; jitter prevents retries from synchronizing across clients.
import google.generativeai as genai
import time
import random
def call_gemini_with_retry(
model_name: str,
prompt: str,
max_retries: int = 5,
base_delay: float = 1.0,
) -> str:
"""
Gemini API call with exponential backoff + jitter.
Automatically retries on overload (503) and rate limit (429) errors.
"""
model = genai.GenerativeModel(model_name)
for attempt in range(max_retries):
try:
response = model.generate_content(prompt)
return response.text
except Exception as e:
error_str = str(e).lower()
# Determine if this error is worth retrying
is_retryable = any(keyword in error_str for keyword in [
"overloaded", "503", "429", "resource exhausted",
"timeout", "unavailable"
])
if not is_retryable or attempt == max_retries - 1:
raise # Non-retryable or out of attempts
# Exponential backoff: 1s, 2s, 4s, 8s, 16s + random jitter
wait_time = base_delay * (2 ** attempt) + random.uniform(0, 1)
print(f"Attempt {attempt + 1} failed ({e}). Retrying in {wait_time:.1f}s...")
time.sleep(wait_time)
raise RuntimeError("Max retries exceeded")
# Usage
genai.configure(api_key="YOUR_GEMINI_API_KEY")
result = call_gemini_with_retry(
model_name="gemini-2.0-flash",
prompt="Summarize this document...",
max_retries=5
)With max_retries=5 and base_delay=1.0, this waits up to 16 seconds plus jitter before the final attempt — enough headroom for transient congestion to clear without blocking your application for too long.
Setting Explicit Timeouts
The Gemini Python SDK doesn't set a default request timeout, which means a stalled connection can hang your application indefinitely. Set timeouts explicitly:
# Using request_options in the SDK
model = genai.GenerativeModel("gemini-2.5-pro")
response = model.generate_content(
"Analyze this long document...",
request_options={"timeout": 60} # seconds
)Reasonable timeout values by use case:
- Short queries, chat responses: 30 seconds
- Document summarization, 2K–4K token output: 60 seconds
- Long-form generation, 8K+ tokens: 90–120 seconds
If you're consistently hitting timeouts on responses that should be fast, the model may be under heavy load. Use the model fallback pattern below.
Model Fallback Strategy
When a high-demand model is overloaded, switching to an alternative model is often faster than waiting for it to recover. A fallback chain handles this automatically:
FALLBACK_MODELS = [
"gemini-2.5-pro", # First choice: highest capability
"gemini-2.0-flash", # Second: faster, less expensive
"gemini-1.5-flash", # Third: older but reliably available
]
def call_with_model_fallback(prompt: str) -> tuple[str, str]:
"""
Try models in priority order, falling back on failure.
Returns (response_text, model_that_responded).
"""
last_error = None
for model_name in FALLBACK_MODELS:
try:
result = call_gemini_with_retry(model_name, prompt, max_retries=3)
return result, model_name
except Exception as e:
last_error = e
print(f"{model_name} unavailable, trying next: {e}")
raise RuntimeError(f"All models failed. Last: {last_error}")
# Usage
text, model_used = call_with_model_fallback("Analyze this data...")
print(f"Response from {model_used}")Adjust the fallback order based on your cost/quality tradeoffs. For latency-sensitive applications, putting Flash first and Pro as the fallback may make more sense than the reverse.
Handling Rate Limits for Batch Processing
If you're processing large batches of requests, sending them all at once will hit RPM limits immediately. A semaphore-based rate limiter keeps concurrent requests within your quota:
import asyncio
import time
from asyncio import Semaphore
class RateLimitedGeminiClient:
"""
Limits concurrent Gemini requests to stay within RPM quotas.
"""
def __init__(self, max_concurrent: int = 10):
self._semaphore = Semaphore(max_concurrent)
async def generate(
self,
model: genai.GenerativeModel,
prompt: str
) -> str:
async with self._semaphore:
# Small delay between requests to avoid bursting
await asyncio.sleep(0.1)
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(
None,
lambda: model.generate_content(prompt)
)
return response.text
async def process_batch(prompts: list[str]) -> list[str]:
client = RateLimitedGeminiClient(max_concurrent=10)
model = genai.GenerativeModel("gemini-2.0-flash")
tasks = [client.generate(model, p) for p in prompts]
return await asyncio.gather(*tasks, return_exceptions=True)
# Process 100 prompts with controlled concurrency
results = asyncio.run(process_batch(your_prompts))For the free tier (often 10–15 RPM depending on model), set max_concurrent to half the RPM limit to leave headroom for retries.
Free Tier Limits Are the Likely Culprit
If overload errors appear consistently right after you start making requests, check your actual quota. Free tier limits are surprisingly low on some models:
gemini-2.5-pro: 5 RPM (free tier)gemini-2.0-flash: 15 RPM (free tier)gemini-1.5-flash: 15 RPM (free tier)
These limits reset per minute, not per second. Three rapid requests to gemini-2.5-pro will exhaust the free-tier RPM immediately. Adding a time.sleep(12) between requests is a quick workaround, but upgrading to a paid tier is the right long-term fix for anything beyond experimentation.
For a broader look at Gemini API implementation patterns, the streaming and chat implementation guide covers async patterns that pair well with the rate limiting approach above.
The Minimal Fix, Right Now
If you just need something working today, copy this into your project:
import time, random
def gemini_safe_call(model, prompt, max_tries=4):
for i in range(max_tries):
try:
return model.generate_content(
prompt,
request_options={"timeout": 60}
).text
except Exception as e:
if i == max_tries - 1: raise
wait = (2 ** i) + random.random()
print(f"Retry {i+1}/{max_tries} in {wait:.1f}s: {e}")
time.sleep(wait)Four lines of logic, handles the common cases. Build on it when you need the model fallback or rate limiting features.
Monitoring Your Error Rate Over Time
Once you have retry logic in place, add basic logging to understand your actual error patterns:
from dataclasses import dataclass, field
from datetime import datetime
from collections import defaultdict
@dataclass
class APICallStats:
total_calls: int = 0
successful_calls: int = 0
retried_calls: int = 0
failed_calls: int = 0
errors_by_type: dict = field(default_factory=lambda: defaultdict(int))
model_usage: dict = field(default_factory=lambda: defaultdict(int))
stats = APICallStats()
def call_gemini_tracked(model_name: str, prompt: str) -> str:
stats.total_calls += 1
for attempt in range(5):
try:
model = genai.GenerativeModel(model_name)
response = model.generate_content(
prompt,
request_options={"timeout": 60}
)
if attempt > 0:
stats.retried_calls += 1
stats.successful_calls += 1
stats.model_usage[model_name] += 1
return response.text
except Exception as e:
error_type = type(e).__name__
stats.errors_by_type[error_type] += 1
if attempt == 4:
stats.failed_calls += 1
raise
wait = (2 ** attempt) + random.uniform(0, 1)
time.sleep(wait)
raise RuntimeError("Unreachable")
# Print stats periodically
def print_stats():
success_rate = (stats.successful_calls / stats.total_calls * 100
if stats.total_calls else 0)
print(f"Calls: {stats.total_calls} | Success: {success_rate:.1f}% | "
f"Retried: {stats.retried_calls} | Failed: {stats.failed_calls}")
print(f"Errors: {dict(stats.errors_by_type)}")Running this for a few hours of production traffic usually reveals whether your overload issues are truly random congestion or systematic quota problems. Systematic quota issues (consistent errors at predictable intervals) point to rate limit problems; random bursts point to congestion that backoff handles well.
A retry rate above 15–20% or a failure rate above 2% in production is a signal to either upgrade your quota, add a fallback model, or reduce concurrency. Below those thresholds, the backoff strategy is absorbing the variance cleanly.
What "Overloaded" Doesn't Mean
One thing worth clarifying: overloaded errors are not a sign that your code is wrong. They're a normal operational characteristic of shared API infrastructure. The error doesn't indicate a bug in your request, a problem with your API key, or content policy issues — those have different error messages.
Treating overloaded as "retry with backoff" rather than "investigate the request" is the right mental model. The goal is to make your application resilient to transient infrastructure conditions, not to debug something you broke.
That said, if you see overload errors consistently on specific request patterns — very long prompts, system instructions with thousands of tokens, or requests that always time out at the same point — that's worth investigating. Extremely large inputs can push individual requests into longer processing queues even when the model isn't broadly overloaded.