Development environments lie. You test with small datasets, send requests one at a time, and everything looks fine. Then production arrives with real traffic, real document sizes, and real concurrent users—and Gemini 2.5 Pro starts showing a completely different personality.
Rate limits, runaway Thinking costs, context quality degradation, and inconsistent accuracy: these are production problems, not API problems. This guide covers each one with working implementations.
Designing for Rate Limits
Rate limits feel theoretical until your first real traffic spike. By then, it's too late to retrofit a proper solution.
Understanding What You're Working With
Gemini 2.5 Pro has three distinct rate limit dimensions:
- RPM (Requests Per Minute): How many API calls per minute
- TPM (Tokens Per Minute): How many tokens processed per minute
- RPD (Requests Per Day): Daily request ceiling
The TPM limit is the one that catches people off-guard on paid tiers—Thinking mode can consume 5–10x the tokens of a standard request, and it comes out of the same TPM budget.
A Rate-Limiting Async Client
The difference between reacting to 429 errors and preventing them is client-side rate tracking:
import asyncio
import google.generativeai as genai
import time
import random
from typing import Optional
class RateLimitedGeminiClient:
"""Async Gemini client with client-side rate limiting."""
def __init__(
self,
api_key: str,
model_name: str = "gemini-2.5-pro",
max_concurrent: int = 5,
rpm_limit: int = 60,
):
genai.configure(api_key=api_key)
self.model = genai.GenerativeModel(model_name)
self.semaphore = asyncio.Semaphore(max_concurrent)
self.rpm_limit = rpm_limit
self.request_times: list[float] = []
def _wait_time_for_rpm(self) -> float:
"""Returns seconds to wait to stay under RPM limit."""
now = time.time()
one_minute_ago = now - 60
self.request_times = [t for t in self.request_times if t > one_minute_ago]
if len(self.request_times) >= self.rpm_limit:
return 60 - (now - self.request_times[0])
return 0
async def generate(self, prompt: str, max_retries: int = 5) -> Optional[str]:
async with self.semaphore:
for attempt in range(max_retries):
wait = self._wait_time_for_rpm()
if wait > 0:
print(f"Approaching RPM limit. Waiting {wait:.1f}s")
await asyncio.sleep(wait)
try:
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(
None, lambda: self.model.generate_content(prompt)
)
self.request_times.append(time.time())
return response.text
except Exception as e:
if "RESOURCE_EXHAUSTED" in str(e) or "429" in str(e):
wait_time = (2 ** attempt) + random.uniform(0, 1)
print(f"Rate limit hit. Retry in {wait_time:.1f}s ({attempt + 1}/{max_retries})")
await asyncio.sleep(wait_time)
else:
raise
return None
async def main():
client = RateLimitedGeminiClient(
api_key="YOUR_API_KEY",
max_concurrent=3,
rpm_limit=50, # Leave a buffer below the actual limit
)
prompts = [
"Explain async/await in Python",
"What are Rust's ownership rules?",
"How does TypeScript's type inference work?",
]
results = await asyncio.gather(*[client.generate(p) for p in prompts])
for prompt, result in zip(prompts, results):
print(f"\nQ: {prompt}")
print(f"A: {result[:200]}..." if result else "A: Failed")
asyncio.run(main())The key design decision: max_concurrent limits simultaneous in-flight requests, while rpm_limit enforces the window-based rate. Set both slightly under the actual limits to give yourself headroom.
Thinking Mode Cost Control
Thinking mode is Gemini 2.5 Pro's most powerful feature and its biggest cost trap.
How Thinking Tokens Work
When Thinking is enabled, the model spends internal tokens on reasoning before generating a response. These "thought tokens" don't appear in the output but count toward your TPM quota and billing.
import google.generativeai as genai
genai.configure(api_key="YOUR_API_KEY")
model = genai.GenerativeModel(
model_name="gemini-2.5-pro",
generation_config={
"thinking_config": {
"thinking_budget": 1024, # Tokens the model can use for reasoning
# 0 = Thinking disabled (fast, cheap)
# -1 = No limit (highest quality, highest cost)
}
}
)
response = model.generate_content("Solve this step by step: ...")
if response.usage_metadata:
print(f"Input tokens: {response.usage_metadata.prompt_token_count}")
print(f"Output tokens: {response.usage_metadata.candidates_token_count}")
if hasattr(response.usage_metadata, 'thoughts_token_count'):
print(f"Thinking tokens: {response.usage_metadata.thoughts_token_count}")Route by Task Type
Not every task benefits from deep reasoning. Routing to the right configuration per task type dramatically reduces costs:
def get_model_for_task(task_type: str) -> genai.GenerativeModel:
"""Returns the appropriate model configuration for a task."""
# Tasks that genuinely benefit from extended reasoning
if task_type in ["complex_reasoning", "math_proof", "code_review", "architecture"]:
return genai.GenerativeModel(
model_name="gemini-2.5-pro",
generation_config={"thinking_config": {"thinking_budget": 8192}}
)
# Tasks where Thinking adds cost but minimal quality improvement
elif task_type in ["summarization", "translation", "extraction", "classification"]:
return genai.GenerativeModel(
model_name="gemini-2.5-pro",
generation_config={"thinking_config": {"thinking_budget": 0}}
)
# Simple tasks where Flash is sufficient
else:
return genai.GenerativeModel(model_name="gemini-2.0-flash")
# Use the right model for the right job
review_model = get_model_for_task("code_review")
summary_model = get_model_for_task("summarization")
review_response = review_model.generate_content("Review this algorithm for correctness...")
summary_response = summary_model.generate_content("Summarize this article in 3 bullets...")In production workloads I've measured, this kind of routing typically reduces token costs by 50–70% compared to using thinking_budget: -1 for everything.
Long-Context Quality Problems
Gemini 2.5 Pro supports up to 1 million tokens of context—sometimes 2 million. But longer context doesn't always mean better answers.
The Needle-in-a-Haystack Problem
Information buried in the middle of a very long document is more likely to be missed than information at the beginning or end. This isn't a bug; it's a property of how attention mechanisms work at scale.
The mitigation is a map-reduce approach: extract relevant information from chunks, then synthesize:
import google.generativeai as genai
genai.configure(api_key="YOUR_API_KEY")
def chunk_and_synthesize(
document: str,
question: str,
chunk_chars: int = 50000,
model_name: str = "gemini-2.5-pro"
) -> str:
"""Extract relevant info from chunks, then synthesize a final answer."""
model = genai.GenerativeModel(model_name)
chunks = [document[i:i + chunk_chars] for i in range(0, len(document), chunk_chars)]
extractions = []
for i, chunk in enumerate(chunks):
prompt = f"""Extract information relevant to this question from the text below.
Question: {question}
Text (chunk {i + 1} of {len(chunks)}):
{chunk}
List relevant facts as bullet points. If nothing is relevant, say "No relevant information"."""
response = model.generate_content(prompt)
text = response.text.strip()
if text != "No relevant information":
extractions.append(f"[Chunk {i + 1}]\n{text}")
if not extractions:
return "No relevant information found in the document."
synthesis_prompt = f"""Based on the extracted information below, provide a comprehensive answer.
Question: {question}
Extracted information:
{chr(10).join(extractions)}
Synthesize a clear, accurate answer. Remove duplicates."""
final = model.generate_content(synthesis_prompt)
return final.text
with open("large_document.txt") as f:
doc = f.read()
answer = chunk_and_synthesize(doc, "What security risks are described in this document?")
print(answer)Structure Context with XML Tags
When sending multiple documents, XML structure helps the model track which information came from where:
def build_structured_context(documents: list[dict]) -> str:
parts = []
for doc in documents:
parts.append(
f'<document id="{doc["id"]}" type="{doc["type"]}" date="{doc.get("date", "unknown")}">\n'
f' <title>{doc["title"]}</title>\n'
f' <content>\n{doc["content"]}\n </content>\n'
f'</document>'
)
return "\n\n".join(parts)
documents = [
{"id": "spec-001", "type": "specification", "date": "2026-01", "title": "API Design Spec", "content": "..."},
{"id": "bug-1234", "type": "bug_report", "date": "2026-03", "title": "Bug Report #1234", "content": "..."},
]
context = build_structured_context(documents)
prompt = f"""{context}
Analyze how the bugs in the bug reports relate to the API design specification.
Cite specific document IDs when referencing information."""
model = genai.GenerativeModel("gemini-2.5-pro")
response = model.generate_content(prompt)
print(response.text)Diagnosing Response Quality Degradation
When accuracy in production is worse than in development, there are four places to look.
Temperature
genai.configure(api_key="YOUR_API_KEY")
# For factual, deterministic outputs (code, data extraction, classification)
precise = genai.GenerativeModel(
"gemini-2.5-pro",
generation_config=genai.GenerationConfig(temperature=0.0, top_p=1.0, top_k=1)
)
# For creative, varied outputs (writing, brainstorming)
creative = genai.GenerativeModel(
"gemini-2.5-pro",
generation_config=genai.GenerationConfig(temperature=1.0, top_p=0.95, top_k=40)
)High temperature on a factual task produces confident but incorrect answers. Low temperature on a creative task produces repetitive output. Match temperature to task type.
System Prompt Specificity
# Vague system prompt → inconsistent outputs
vague = "You are a helpful assistant. Be polite and accurate."
# Specific system prompt → consistent, high-quality outputs
specific = """You are a senior Python code reviewer with expertise in security and performance.
## Your task
For each code submission, provide structured feedback on:
1. Bugs and potential runtime errors
2. Security vulnerabilities
3. Performance bottlenecks
4. Readability and maintainability
## Output format
For each issue found:
- **Severity**: Critical / Warning / Suggestion
- **Location**: Line number or function name
- **Problem**: Specific description
- **Fix**: Recommended correction with a complete code example
## Constraints
- If no issues found, explicitly state "No issues found"
- Base all feedback on concrete evidence, not preference
- Assume Python 3.10+"""
model = genai.GenerativeModel(
"gemini-2.5-pro",
system_instruction=specific
)Few-Shot Examples for Consistent Formatting
def build_few_shot_prompt(question: str, examples: list[dict]) -> list:
contents = []
for ex in examples:
contents.append({"role": "user", "parts": [{"text": ex["input"]}]})
contents.append({"role": "model", "parts": [{"text": ex["output"]}]})
contents.append({"role": "user", "parts": [{"text": question}]})
return contents
# Example: consistent JSON classification output
examples = [
{
"input": "Classify: 'The API response is too slow'",
"output": '{"category": "performance", "severity": "medium", "confidence": 0.92}'
},
{
"input": "Classify: 'SQL injection possible in login endpoint'",
"output": '{"category": "security", "severity": "critical", "confidence": 0.98}'
}
]
contents = build_few_shot_prompt(
"Classify: 'Memory usage spikes every 4 hours and crashes the service'",
examples
)
model = genai.GenerativeModel("gemini-2.5-pro")
response = model.generate_content(contents=contents)
print(response.text)
# Expected: {"category": "performance", "severity": "critical", "confidence": ...}Monitoring for Early Warning Signs
Quality problems are easier to fix when you catch them early.
import time
import statistics
from collections import deque
from dataclasses import dataclass
from typing import Optional
@dataclass
class RequestMetrics:
timestamp: float
prompt_tokens: int
response_tokens: int
thinking_tokens: int
latency_ms: float
success: bool
error_type: Optional[str] = None
class GeminiMonitor:
def __init__(self, window: int = 100):
self.metrics: deque[RequestMetrics] = deque(maxlen=window)
def record(self, m: RequestMetrics):
self.metrics.append(m)
if self.error_rate() > 0.10:
print(f"⚠️ Error rate {self.error_rate() * 100:.1f}% — investigate immediately")
def error_rate(self) -> float:
if not self.metrics: return 0.0
return sum(1 for m in self.metrics if not m.success) / len(self.metrics)
def p95_latency(self) -> float:
latencies = sorted(m.latency_ms for m in self.metrics if m.success)
if not latencies: return 0.0
idx = int(len(latencies) * 0.95)
return latencies[idx]
def tokens_per_minute(self) -> float:
now = time.time()
recent = [m for m in self.metrics if now - m.timestamp < 60]
return sum(m.prompt_tokens + m.response_tokens + m.thinking_tokens for m in recent)
def summary(self) -> dict:
return {
"requests": len(self.metrics),
"error_rate": f"{self.error_rate() * 100:.1f}%",
"p95_latency_ms": f"{self.p95_latency():.0f}",
"tokens_per_minute": f"{self.tokens_per_minute():.0f}",
}
monitor = GeminiMonitor()
def monitored_generate(model, prompt: str) -> Optional[str]:
t0 = time.time()
try:
response = model.generate_content(prompt)
latency = (time.time() - t0) * 1000
usage = response.usage_metadata
monitor.record(RequestMetrics(
timestamp=t0,
prompt_tokens=usage.prompt_token_count if usage else 0,
response_tokens=usage.candidates_token_count if usage else 0,
thinking_tokens=getattr(usage, 'thoughts_token_count', 0) if usage else 0,
latency_ms=latency,
success=True,
))
return response.text
except Exception as e:
latency = (time.time() - t0) * 1000
monitor.record(RequestMetrics(
timestamp=t0, prompt_tokens=0, response_tokens=0, thinking_tokens=0,
latency_ms=latency, success=False, error_type=type(e).__name__
))
raiseProduction reliability with Gemini 2.5 Pro comes down to three investments: client-side rate management so you stop reacting to 429s, task-aware Thinking budget control so costs don't creep up, and structured context so the model can actually find the information it needs.
Start with monitoring. You can't optimize what you can't measure, and most production issues reveal themselves clearly once you have latency and error rate data in front of you.