Deploying Agents to Production
What is Agent Deployment?
Agent deployment transforms development prototypes into production-ready services. It encompasses containerization, API serving, monitoring, scaling, security, and operational reliability. The goal is five-nines availability with automatic recovery.
The deployment stack typically includes: FastAPI for HTTP serving, Docker for containerization, Redis for caching and sessions, Prometheus/Grafana for monitoring, and Kubernetes or ECS for orchestration.
Production agents differ from prototypes in: error handling, rate limiting, authentication, logging, health checks, graceful degradation, and cost monitoring.
Project Overview
We will deploy an agent with:
- FastAPI REST API with async support
- Docker containerization
- Redis caching and session management
- Prometheus metrics and Grafana dashboards
- Auto-scaling configuration
- Health checks and circuit breakers
- Structured logging
Expected outcome: A production deployment template for any agent.
Difficulty: Advanced (requires understanding of DevOps, containerization, and production operations)
Architecture
Tools & Setup
| Tool | Version | Purpose |
|---|---|---|
| Python | 3.11+ | Core language |
| FastAPI | 0.109+ | HTTP framework |
| uvicorn | 0.27+ | ASGI server |
| redis | 5.0+ | Caching |
| docker | 24.0+ | Containerization |
| prometheus-client | 0.19+ | Metrics |
Step 1: Environment Setup
python -m venv venv
source venv/bin/activate
pip install fastapi uvicorn redis prometheus-client pydantic httpx
Step 2: Project Structure
deploy-agent/
βββ app/
β βββ __init__.py
β βββ main.py
β βββ agent.py
β βββ routes.py
β βββ middleware.py
β βββ config.py
β βββ metrics.py
βββ Dockerfile
βββ docker-compose.yml
βββ requirements.txt
βββ k8s/
βββ deployment.yaml
Step 3: FastAPI Application
# app/main.py
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
import redis.asyncio as redis
app = FastAPI(title="Agent API", version="1.0.0")
redis_client = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global redis_client
redis_client = redis.from_url("redis://localhost:6379", decode_responses=True)
yield
await redis_client.close()
app = FastAPI(title="Agent API", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health")
async def health():
return {"status": "healthy", "version": "1.0.0"}
@app.get("/ready")
async def ready():
try:
await redis_client.ping()
return {"status": "ready"}
except:
raise HTTPException(status_code=503, detail="Not ready")
# app/routes.py
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from app.agent import ProductionAgent
from app.metrics import track_request
router = APIRouter()
agent = ProductionAgent()
class QueryRequest(BaseModel):
query: str
user_id: str = "anonymous"
session_id: str = None
class QueryResponse(BaseModel):
answer: str
request_id: str
latency_ms: float
tokens_used: int
@router.post("/query", response_model=QueryResponse)
@track_request
async def query(request: QueryRequest):
result = await agent.process(request.query, request.user_id)
return QueryResponse(**result)
# app/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "Agent API"
debug: bool = False
redis_url: str = "redis://localhost:6379"
openai_api_key: str
max_concurrent_requests: int = 100
request_timeout: int = 30
class Config:
env_file = ".env"
settings = Settings()
# app/metrics.py
from prometheus_client import Counter, Histogram
import time
from functools import wraps
REQUEST_COUNT = Counter("agent_requests_total", "Total requests", ["method", "endpoint", "status"])
REQUEST_LATENCY = Histogram("agent_request_latency_seconds", "Request latency", ["endpoint"])
TOKEN_USAGE = Counter("agent_tokens_used_total", "Total tokens used", ["model"])
def track_request(func):
@wraps(func)
async def wrapper(*args, **kwargs):
start = time.time()
try:
result = await func(*args, **kwargs)
REQUEST_COUNT.labels(method="POST", endpoint="/query", status="success").inc()
return result
except Exception as e:
REQUEST_COUNT.labels(method="POST", endpoint="/query", status="error").inc()
raise
finally:
latency = time.time() - start
REQUEST_LATENCY.labels(endpoint="/query").observe(latency)
return wrapper
Step 4: Production Agent with Caching
# app/agent.py
from openai import AsyncOpenAI
import hashlib
import json
from typing import Dict
class ProductionAgent:
def __init__(self):
self.client = AsyncOpenAI()
self.model = "gpt-4-turbo-preview"
async def process(self, query: str, user_id: str) -> Dict:
cache_key = f"query:{hashlib.md5(query.encode()).hexdigest()}"
cached = await self._get_cache(cache_key)
if cached:
return cached
response = await self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": query},
],
temperature=0.7,
max_tokens=1000,
)
result = {
"answer": response.choices[0].message.content,
"request_id": f"req_{hashlib.md5(query.encode()).hexdigest()[:8]}",
"latency_ms": 0,
"tokens_used": response.usage.total_tokens,
}
await self._set_cache(cache_key, result, ttl=300)
return result
async def _get_cache(self, key: str):
try:
from app.main import redis_client
data = await redis_client.get(key)
return json.loads(data) if data else None
except:
return None
async def _set_cache(self, key: str, value: Dict, ttl: int = 300):
try:
from app.main import redis_client
await redis_client.setex(key, ttl, json.dumps(value))
except:
pass
Step 5: Docker and Kubernetes
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
# docker-compose.yml
version: '3.8'
services:
api:
build: .
ports:
- "8000:8000"
environment:
- REDIS_URL=redis://redis:6379
- OPENAI_API_KEY=${OPENAI_API_KEY}
depends_on:
- redis
deploy:
replicas: 3
redis:
image: redis:7-alpine
ports:
- "6379:6379"
prometheus:
image: prom/prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana
ports:
- "3000:3000"
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: agent-api
spec:
replicas: 3
selector:
matchLabels:
app: agent-api
template:
metadata:
labels:
app: agent-api
spec:
containers:
- name: agent-api
image: agent-api:latest
ports:
- containerPort: 8000
env:
- name: REDIS_URL
value: "redis://redis-service:6379"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 5
readinessProbe:
httpGet:
path: /ready
port: 8000
initialDelaySeconds: 5
periodSeconds: 3
---
apiVersion: v1
kind: Service
metadata:
name: agent-api-service
spec:
selector:
app: agent-api
ports:
- port: 80
targetPort: 8000
type: LoadBalancer
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: agent-api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: agent-api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
Mathematical Foundation
Capacity Planning:
Intuition: Number of pods needed to handle expected load at target utilization.
Auto-scaling Threshold:
Intuition: Scale up when CPU exceeds 120% of target to handle increasing load.
Testing & Evaluation
import pytest
from httpx import AsyncClient
from app.main import app
@pytest.mark.asyncio
async def test_health():
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.get("/health")
assert response.status_code == 200
@pytest.mark.asyncio
async def test_query():
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post("/query", json={"query": "test"})
assert response.status_code == 200
Performance Metrics
| Metric | Value | Notes |
|---|---|---|
| Request Latency | 200ms-2s | Depends on LLM |
| Throughput | 100+ RPS | Per pod |
| Cache Hit Rate | 30-60% | Depends on query patterns |
| Memory Usage | 256-512MB | Per pod |
| Startup Time | 5-10s | Cold start |
Real-World Use Cases
- SaaS Products: Multi-tenant agent services
- Internal Tools: Company-wide agent deployment
- API Services: Agent-as-a-service offerings
- Edge Deployment: Low-latency regional deployment
- Hybrid Cloud: On-premise + cloud deployment
Common Pitfalls & Solutions
| Pitfall | Solution |
|---|---|
| Cold start latency | Pre-warm containers, use provisioned concurrency |
| Memory leaks | Monitor memory, implement restart policies |
| Rate limiting | Implement per-user quotas |
| Cost overruns | Set spending limits, monitor usage |
| Single point of failure | Multi-AZ deployment, redundancy |
Summary with Key Takeaways
- FastAPI provides high-performance async HTTP serving
- Docker ensures consistent environments across development and production
- Redis caching reduces LLM costs and improves latency
- Kubernetes auto-scaling handles traffic spikes
- Comprehensive monitoring enables proactive issue detection