Setup and context: Why Gemini × FastAPI?
Gemini 2.5 Pro API is one of the most versatile LLM APIs available today, featuring a 1M-token context window, native multimodal support, and powerful Function Calling capabilities. FastAPI, on the other hand, is the fastest and most async-friendly Python web framework — the perfect choice for AI backend development.
Combining these two gives you a production-grade AI backend that can handle real-world scale. This guide goes beyond a basic chatbot implementation: we'll cover streaming responses, rate limiting, retry logic, Function Calling, cost optimization, and Docker deployment — everything you need to ship to production.
By the end of this article, you'll be able to:
- Build streaming endpoints with FastAPI and the Gemini API
- Handle rate limits gracefully using exponential backoff
- Implement tool-calling backends with Function Calling
- Deploy your service with Docker
- Minimize API costs with proven optimization techniques
Target audience: Engineers familiar with Python and FastAPI basics who want to deploy Gemini API in production.
Prerequisites & Environment Setup
What You'll Need
- Python 3.11+
- Gemini API key (get one at Google AI Studio)
- Docker (for production deployment)
- Basic understanding of async/await in Python
Setting Up Your Environment
# Create project directory
mkdir gemini-fastapi-backend && cd gemini-fastapi-backend
# Create and activate virtual environment
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# Install dependencies
pip install fastapi uvicorn google-generativeai tenacity python-dotenv pydantic redisCreate a .env file for API key management:
# .env
GEMINI_API_KEY=your_api_key_here
REDIS_URL=redis://localhost:6379
ENVIRONMENT=production
MAX_RETRIES=3Project Structure
gemini-fastapi-backend/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI application entry point
│ ├── config.py # Settings management
│ ├── models.py # Pydantic models
│ ├── services/
│ │ ├── gemini.py # Gemini API client
│ │ └── tools.py # Function Calling tool definitions
│ └── routers/
│ ├── chat.py # Chat endpoints
│ └── stream.py # Streaming endpoints
├── Dockerfile
├── docker-compose.yml
└── requirements.txt
Architectural Concepts
Why Async Matters
Requests to the Gemini API are I/O-bound operations with significant network wait times. Using FastAPI's async/await correctly lets a single worker process handle hundreds of concurrent requests. Compared to synchronous implementations, you can see 10-50x improvement in throughput — a critical advantage for production AI backends.
The Value of Streaming
Large language models generate tokens sequentially. Without streaming, you have to wait until the very last token before sending a response — especially painful with long outputs from Gemini 2.5 Pro. Server-Sent Events (SSE)-based streaming delivers tokens to users the moment they're generated, dramatically improving perceived performance.
Step-by-Step Implementation
Step 1: Configuration & Gemini Client with Retry Logic
# app/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
gemini_api_key: str
environment: str = "development"
max_retries: int = 3
redis_url: str = "redis://localhost:6379"
# Gemini model settings
default_model: str = "gemini-2.5-pro"
max_output_tokens: int = 8192
temperature: float = 0.7
class Config:
env_file = ".env"
settings = Settings()# app/services/gemini.py
import asyncio
import google.generativeai as genai
from tenacity import (
retry,
stop_after_attempt,
wait_exponential,
retry_if_exception_type,
)
from google.api_core.exceptions import ResourceExhausted, ServiceUnavailable
from app.config import settings
import logging
logger = logging.getLogger(__name__)
genai.configure(api_key=settings.gemini_api_key)
class GeminiClient:
def __init__(self, model_name: str = None):
self.model_name = model_name or settings.default_model
self.generation_config = genai.types.GenerationConfig(
max_output_tokens=settings.max_output_tokens,
temperature=settings.temperature,
)
self.model = genai.GenerativeModel(
model_name=self.model_name,
generation_config=self.generation_config,
)
@retry(
stop=stop_after_attempt(settings.max_retries),
wait=wait_exponential(multiplier=1, min=4, max=60),
retry=retry_if_exception_type((ResourceExhausted, ServiceUnavailable)),
before_sleep=lambda retry_state: logger.warning(
f"Rate limited. Retrying in {retry_state.next_action.sleep}s "
f"(attempt {retry_state.attempt_number}/{settings.max_retries})"
),
)
async def generate(self, prompt: str, system_instruction: str = None) -> str:
"""
Content generation with exponential backoff retry.
Automatically retries on ResourceExhausted (429) or ServiceUnavailable (503).
"""
model = self.model
if system_instruction:
model = genai.GenerativeModel(
model_name=self.model_name,
generation_config=self.generation_config,
system_instruction=system_instruction,
)
# Use asyncio.to_thread to run the blocking SDK call asynchronously
response = await asyncio.to_thread(
model.generate_content, prompt
)
return response.text
async def stream_generate(self, prompt: str, system_instruction: str = None):
"""
Streaming generation. Yields each chunk as an async generator.
"""
model = self.model
if system_instruction:
model = genai.GenerativeModel(
model_name=self.model_name,
generation_config=self.generation_config,
system_instruction=system_instruction,
)
response = await asyncio.to_thread(
model.generate_content, prompt, stream=True
)
for chunk in response:
if chunk.text:
yield chunk.text
gemini_client = GeminiClient()Step 2: Streaming Endpoint
# app/routers/stream.py
from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from app.services.gemini import gemini_client
import json
import asyncio
router = APIRouter(prefix="/api/v1", tags=["streaming"])
class StreamRequest(BaseModel):
message: str
system_instruction: str | None = None
session_id: str | None = None
async def event_generator(request: StreamRequest):
"""
Generates streaming responses in Server-Sent Events (SSE) format.
Each chunk is sent as: {"type": "chunk", "content": "..."}
Errors are sent as: {"type": "error", "message": "..."}
Completion is signaled by: {"type": "done"}
"""
try:
async for chunk in gemini_client.stream_generate(
prompt=request.message,
system_instruction=request.system_instruction,
):
data = json.dumps({"type": "chunk", "content": chunk})
yield f"data: {data}\n\n"
# Yield control to prevent buffer overflow
await asyncio.sleep(0)
yield f"data: {json.dumps({'type': 'done'})}\n\n"
except Exception as e:
error_data = json.dumps({"type": "error", "message": str(e)})
yield f"data: {error_data}\n\n"
@router.post("/stream")
async def stream_chat(request: StreamRequest):
"""
Streaming chat endpoint using Server-Sent Events.
Example client-side usage (JavaScript):
const eventSource = new EventSource('/api/v1/stream');
eventSource.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.type === 'chunk') process.stdout.write(data.content);
};
"""
if not request.message.strip():
raise HTTPException(status_code=400, detail="Message cannot be empty")
return StreamingResponse(
event_generator(request),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no", # Disable Nginx buffering
},
)Step 3: Function Calling Implementation
# app/services/tools.py
import google.generativeai as genai
import asyncio
TOOLS = [
genai.protos.Tool(
function_declarations=[
genai.protos.FunctionDeclaration(
name="get_current_weather",
description="Get current weather information for a specified city",
parameters=genai.protos.Schema(
type=genai.protos.Type.OBJECT,
properties={
"city": genai.protos.Schema(
type=genai.protos.Type.STRING,
description="City name (e.g., Tokyo, New York)",
),
"unit": genai.protos.Schema(
type=genai.protos.Type.STRING,
enum=["celsius", "fahrenheit"],
description="Temperature unit",
),
},
required=["city"],
),
),
genai.protos.FunctionDeclaration(
name="search_articles",
description="Search for articles on Gemini Lab",
parameters=genai.protos.Schema(
type=genai.protos.Type.OBJECT,
properties={
"query": genai.protos.Schema(
type=genai.protos.Type.STRING,
description="Search query",
),
"limit": genai.protos.Schema(
type=genai.protos.Type.INTEGER,
description="Number of articles to return (default: 5)",
),
},
required=["query"],
),
),
]
)
]
async def execute_tool(function_name: str, args: dict) -> dict:
"""Execute the tool called by Function Calling"""
if function_name == "get_current_weather":
city = args.get("city", "Tokyo")
unit = args.get("unit", "celsius")
# In production, call a real weather API like Open-Meteo
return {
"city": city,
"temperature": 22,
"unit": unit,
"condition": "Sunny",
"humidity": 55,
}
elif function_name == "search_articles":
query = args.get("query", "")
limit = args.get("limit", 5)
return {
"results": [
{
"title": f"Complete Guide to Gemini {query}",
"url": f"(/articles/gemini-api/{query.lower().replace(' ', '-')}",
}
],
"total": 1,
}
return {"error": f"Unknown function: {function_name}"}# app/routers/chat.py
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
import google.generativeai as genai
import asyncio
from app.config import settings
from app.services.tools import TOOLS, execute_tool
import logging
router = APIRouter(prefix="/api/v1", tags=["chat"])
logger = logging.getLogger(__name__)
class ChatRequest(BaseModel):
message: str
history: list[dict] = []
use_tools: bool = True
class ChatResponse(BaseModel):
response: str
tool_calls: list[dict] = []
model: str
tokens_used: int | None = None
@router.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
"""
Chat endpoint with Function Calling support.
Executes multi-turn reasoning while calling external tools as needed.
Expected response:
{
"response": "The weather in Tokyo is currently 22°C and sunny.",
"tool_calls": [{"function": "get_current_weather", "args": {"city": "Tokyo"}}],
"model": "gemini-2.5-pro",
"tokens_used": 342
}
"""
genai.configure(api_key=settings.gemini_api_key)
model = genai.GenerativeModel(
model_name=settings.default_model,
tools=TOOLS if request.use_tools else None,
)
chat_history = [
{"role": msg["role"], "parts": [msg["content"]]}
for msg in request.history
]
chat_session = model.start_chat(history=chat_history)
tool_calls_log = []
response = await asyncio.to_thread(chat_session.send_message, request.message)
# Function Calling loop
while (
response.candidates and
response.candidates[0].content.parts and
hasattr(response.candidates[0].content.parts[0], 'function_call') and
response.candidates[0].content.parts[0].function_call.name
):
fc = response.candidates[0].content.parts[0].function_call
func_name = fc.name
func_args = dict(fc.args)
logger.info(f"Tool call: {func_name}({func_args})")
tool_calls_log.append({"function": func_name, "args": func_args})
tool_result = await execute_tool(func_name, func_args)
response = await asyncio.to_thread(
chat_session.send_message,
genai.protos.Part(
function_response=genai.protos.FunctionResponse(
name=func_name,
response={"result": tool_result},
)
)
)
tokens_used = None
if hasattr(response, 'usage_metadata'):
tokens_used = response.usage_metadata.total_token_count
return ChatResponse(
response=response.text,
tool_calls=tool_calls_log,
model=settings.default_model,
tokens_used=tokens_used,
)Step 4: FastAPI Application Assembly
# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
import logging
from app.routers import chat, stream
from app.config import settings
logging.basicConfig(
level=logging.INFO if settings.environment == "production" else logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
@asynccontextmanager
async def lifespan(app: FastAPI):
logging.info(f"🚀 Starting Gemini FastAPI Backend (env: {settings.environment})")
yield
logging.info("🛑 Shutting down Gemini FastAPI Backend")
app = FastAPI(
title="Gemini FastAPI Backend",
description="Production-ready AI backend powered by Gemini 2.5 Pro × FastAPI",
version="1.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["https://yourdomain.com"] if settings.environment == "production" else ["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(chat.router)
app.include_router(stream.router)
@app.get("/health")
async def health_check():
"""Health check endpoint for load balancers"""
return {"status": "ok", "model": settings.default_model}
# Expected output (curl http://localhost:8000/health):
# {"status": "ok", "model": "gemini-2.5-pro"}Advanced Patterns
Adaptive Rate Limiting
For production environments with multiple concurrent clients, you need a more sophisticated rate limiter that adapts to API responses:
# app/middleware/rate_limit.py
import asyncio
import time
class AdaptiveRateLimiter:
"""
Adaptive rate limiter that automatically widens request intervals
when 429 errors increase, and tightens them as successes accumulate.
"""
def __init__(self, initial_rps: float = 10.0):
self.min_interval = 1.0 / initial_rps
self.current_interval = self.min_interval
self.last_request_time = 0.0
self.lock = asyncio.Lock()
self._consecutive_errors = 0
self._consecutive_successes = 0
async def acquire(self):
async with self.lock:
now = time.monotonic()
elapsed = now - self.last_request_time
if elapsed < self.current_interval:
await asyncio.sleep(self.current_interval - elapsed)
self.last_request_time = time.monotonic()
def record_success(self):
self._consecutive_successes += 1
self._consecutive_errors = 0
if self._consecutive_successes >= 10:
self.current_interval = max(self.min_interval, self.current_interval * 0.9)
self._consecutive_successes = 0
def record_rate_limit(self):
self._consecutive_errors += 1
self._consecutive_successes = 0
self.current_interval = min(60.0, self.current_interval * 2.0)
rate_limiter = AdaptiveRateLimiter(initial_rps=5.0)Response Caching for Cost Reduction
Caching responses for identical prompts can dramatically cut your API costs:
# app/services/cache.py
import hashlib
import json
from datetime import timedelta
import redis.asyncio as redis
from app.config import settings
class ResponseCache:
def __init__(self):
self.client = redis.from_url(settings.redis_url)
self.ttl = timedelta(hours=24)
def _make_key(self, prompt: str, model: str) -> str:
content = f"{model}:{prompt}"
return f"gemini:cache:{hashlib.sha256(content.encode()).hexdigest()}"
async def get(self, prompt: str, model: str) -> str | None:
key = self._make_key(prompt, model)
cached = await self.client.get(key)
return json.loads(cached) if cached else None
async def set(self, prompt: str, model: str, response: str):
key = self._make_key(prompt, model)
await self.client.setex(
key,
int(self.ttl.total_seconds()),
json.dumps(response)
)
cache = ResponseCache()Troubleshooting
ResourceExhausted (429 Too Many Requests)
This is the most common production issue. The tenacity-based retry logic from Step 1 handles it automatically. For free-tier limits, note that Gemini 2.5 Pro is capped at 60 RPM / 1,000 RPD. Upgrade to a paid plan or implement the adaptive rate limiter above for higher throughput.
ValueError: Invalid operation: The response.text quick accessor
This happens when you try to access response.text directly on a streaming response. Always use chunk.text when iterating over stream chunks.
Asyncio timeout on long requests
For prompts that generate very long responses, add an explicit timeout:
async def generate_with_timeout(prompt: str, timeout: float = 120.0) -> str:
try:
return await asyncio.wait_for(
asyncio.to_thread(model.generate_content, prompt),
timeout=timeout
)
except asyncio.TimeoutError:
raise HTTPException(status_code=504, detail="API timeout (120s exceeded)")Docker Deployment
Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Security: run as non-root user
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
USER appuser
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
EXPOSE 8000
# Production: recommended workers = (CPU count × 2) + 1
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]docker-compose.yml
version: "3.9"
services:
api:
build: .
ports:
- "8000:8000"
environment:
- GEMINI_API_KEY=${GEMINI_API_KEY}
- REDIS_URL=redis://redis:6379
- ENVIRONMENT=production
depends_on:
redis:
condition: service_healthy
restart: unless-stopped
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
# Launch with:
# GEMINI_API_KEY=your_key docker-compose up -dCost & Performance Considerations
Gemini 2.5 Pro Pricing (as of March 2026)
| Use case | Model | Input cost | Output cost |
|---|---|---|---|
| Long context | Gemini 2.5 Pro (>200K tokens) | $2.50/1M tokens | $15.00/1M tokens |
| Standard | Gemini 2.5 Pro (≤200K tokens) | $1.25/1M tokens | $10.00/1M tokens |
| Cost-efficient | Gemini 2.5 Flash | $0.075/1M tokens | $0.30/1M tokens |
Cost Reduction Best Practices
- Caching: Use Redis to cache responses for identical or near-identical queries (see Step 3)
- Model routing: Route simple tasks to Gemini 2.5 Flash; reserve Pro for complex reasoning
- Context Caching: Cache long system instructions and shared context using the [Context Caching API]((/articles/gemini-api/context-caching-guide)
- Batch processing: For non-real-time workloads, use the Batch API to cut costs in half
- Prompt optimization: Remove redundancy from prompts to reduce token counts
Summary & Next Steps
In this guide, we built a production-ready AI backend using Gemini 2.5 Pro × FastAPI, covering:
- Async architecture: Integrating the synchronous SDK with FastAPI via
asyncio.to_thread - Streaming: Real-time token delivery with Server-Sent Events
- Rate limiting: Stability via
tenacityretry logic and adaptive rate limiting - Function Calling: Agentic behavior through tool integration
- Cost optimization: Caching, model routing, and batch processing
- Docker deployment: Container-ready production configuration
For your next steps, explore these related articles:
- [Gemini API Function Calling in Production]((/articles/gemini-api/gemini-function-calling-production) — Advanced Function Calling patterns
- [Vertex AI × Gemini Production Deployment]((/articles/gemini-dev/vertex-ai-gemini-production) — Enterprise-scale deployment strategies
- [Gemini API Cost Optimization Guide]((/articles/gemini-api/gemini-api-cost-optimization) — Deep dive into cost reduction techniques