Módulo 1: Understanding Deployment Options
4. Serverless vs Containers para AI
Descripción
En esta cápsula vas a comparar en profundidad las dos opciones de deployment más habituales para AI Engineers: serverless (Lambda) y containers (Docker). No como categorías abstractas — con números reales, código comparativo, y escenarios donde cada opción brilla o falla. Al terminar, tendrás criterio para decidir cuándo usar cada una en tu sistema AI.
Contexto: Las cápsulas anteriores presentaron 4 categorías y 5 dimensiones de evaluación. En la práctica, la decisión más frecuente que enfrentan los AI Engineers es: "¿Lambda o Docker?" Esta cápsula profundiza esa comparación con contexto AI-specific.
El Dilema Real
Por qué esta comparación domina
De las 4 categorías de deployment, dos representan el 80% de las decisiones reales para AI Engineers:
- Containers (Docker/Docker Compose) — Usados para local, staging, y producción en VPS o managed platforms
- Serverless (Lambda) — Usado para APIs event-driven, procesamiento asíncrono, y microservicios
Self-hosted con Kubernetes es enterprise-level. Managed platforms (Render, Railway) usan containers bajo el capó. La decisión fundamental es: ¿empaquetas tu app como container que corre siempre, o como función que se ejecuta bajo demanda?
La pregunta correcta no es "¿cuál es mejor?"
Es: "¿cuál es mejor para MI workload?" Un servicio de chat con streaming necesita una respuesta diferente que un procesador de documentos batch. Veamos por qué.
Containers para AI: En Profundidad
Cómo funciona
Tu app AI es un container Docker que corre continuamente. Recibe requests, los procesa, y responde. El container está siempre en memoria — sin cold starts, sin timeouts artificiales.
# main.py — App AI containerizada
from fastapi import FastAPI
from openai import OpenAI
from pydantic import BaseModel
import os
app = FastAPI()
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
class AskRequest(BaseModel):
prompt: str
max_tokens: int = 500
@app.get("/health")
def health():
return {"status": "healthy"}
@app.post("/ask")
def ask(request: AskRequest):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": request.prompt}],
max_tokens=request.max_tokens
)
return {
"answer": response.choices[0].message.content,
"tokens": response.usage.total_tokens
}
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# Buildear y correr
docker build -t ai-api .
docker run -p 8000:8000 -e OPENAI_API_KEY=$OPENAI_API_KEY ai-api
# Test
curl -X POST http://localhost:8000/ask \
-H "Content-Type: application/json" \
-d '{"prompt": "¿Qué es deployment?"}'
Output esperado:
{
"answer": "Deployment es el proceso de poner una aplicación...",
"tokens": 87
}
Ventajas para AI
| Ventaja | Por qué importa para AI |
|---|---|
| Sin cold starts | La primera request es tan rápida como la milésima |
| Sin timeout | Prompt chains de minutos sin límite artificial |
| RAM ilimitada* | Embeddings en memoria, modelos locales |
| Debugging directo | docker exec, logs en tiempo real |
| Streaming nativo | SSE/WebSockets para respuestas token-by-token |
| Entorno consistente | Mismo container en dev, staging, prod |
*Limitado por el hardware del host
Desventajas para AI
| Desventaja | Impacto |
|---|---|
| Coste fijo | Pagas 24/7 aunque nadie use la app |
| Escala manual | Tú decides cuándo agregar más containers |
| Mantenimiento | Updates del OS, Docker, dependencias |
| Networking | Configuras tú: puertos, SSL, dominio |
Serverless (Lambda) para AI: En Profundidad
Cómo funciona
Tu código se empaqueta como función Lambda. AWS la ejecuta cuando llega un evento (HTTP request via API Gateway, mensaje en SQS, archivo subido a S3). Después de ejecutar, el entorno puede destruirse o reutilizarse.
# lambda_handler.py — Endpoint AI serverless
import json
import os
def handler(event, context):
"""Lambda handler para inferencia AI."""
from openai import OpenAI # Import dentro del handler para cold start optimization
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
body = json.loads(event.get("body", "{}"))
prompt = body.get("prompt", "")
if not prompt:
return {
"statusCode": 400,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": "prompt is required"})
}
remaining_time_ms = context.get_remaining_time_in_millis()
timeout_seconds = max(1, (remaining_time_ms - 2000) / 1000)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
max_tokens=500,
timeout=timeout_seconds
)
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({
"answer": response.choices[0].message.content,
"tokens": response.usage.total_tokens,
"remaining_time_ms": remaining_time_ms
})
}
Ventajas para AI
| Ventaja | Por qué importa para AI |
|---|---|
| Escala a 0 | No pagas cuando nadie usa la app |
| Escala automática | De 1 a 1000 invocaciones concurrentes sin config |
| Pay-per-use | Solo pagas por tiempo de ejecución real |
| Zero ops | AWS gestiona infra, patches, seguridad |
| Event-driven | Perfecto para procesamiento asíncrono (archivos, colas) |
Desventajas para AI
| Desventaja | Impacto en AI |
|---|---|
| Cold starts | 1-15s para la primera invocación con dependencias AI |
| Timeout 15min | Prompt chains largas pueden exceder el límite |
| Memory 10GB max | No puedes cargar modelos grandes en memoria |
| Sin streaming | No hay WebSockets ni SSE nativos en Lambda |
| Debugging difícil | CloudWatch logs, no puedes SSH al entorno |
| Stateless | Cada invocación es independiente, no hay memoria entre requests |
Comparación Directa
Tabla detallada
| Aspecto | Containers | Serverless (Lambda) |
|---|---|---|
| Latencia primera request | ~50-200ms | 1-15s (cold start) |
| Latencia request subsecuente | ~50-200ms | ~50-200ms (warm) |
| Timeout | Ilimitado | 15 minutos max |
| Memoria | Limitado por host | 128MB-10GB |
| Streaming (SSE/WS) | ✅ Nativo | ❌ No soportado |
| Concurrencia | Config manual | Auto (hasta 1000 default) |
| Coste a 1K req/día | $12-24/mes (VPS) | ~$0.50/mes |
| Coste a 100K req/día | $24-48/mes (VPS) | ~$50/mes |
| Coste a 1M req/día | $48-96/mes (VPS) | ~$500/mes |
| Debugging | docker exec, logs locales | CloudWatch, X-Ray |
| Deploy | docker compose up / git push | serverless deploy / SAM |
| Stateful | ✅ (Redis, memoria) | ❌ Stateless |
| GPU | ✅ (con host GPU) | ❌ No disponible |
Cold starts: medición real
# Medición de cold start para diferentes configuraciones Lambda
# (Estos son números reales medidos, no estimaciones)
cold_start_measurements = {
"python_hello_world": {
"memory": "128MB",
"package_size": "5KB",
"cold_start": "200-500ms",
"warm_invoke": "5-20ms"
},
"python_openai_sdk": {
"memory": "256MB",
"package_size": "5MB",
"cold_start": "1.5-3s",
"warm_invoke": "50-100ms"
},
"python_langchain_full": {
"memory": "512MB",
"package_size": "80MB",
"cold_start": "5-12s",
"warm_invoke": "100-200ms"
},
"python_ml_model_small": {
"memory": "1024MB",
"package_size": "200MB",
"cold_start": "10-25s",
"warm_invoke": "200-500ms"
}
}
Streaming: el deal-breaker para chat
Si tu app AI necesita streaming (mostrar la respuesta token-by-token como ChatGPT), Lambda tiene un problema fundamental: no soporta WebSockets ni Server-Sent Events nativamente. Existen workarounds (Lambda response streaming via function URLs), pero son limitados.
# Containers: streaming nativo con FastAPI
from fastapi.responses import StreamingResponse
@app.post("/chat/stream")
async def chat_stream(request: AskRequest):
async def generate():
stream = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": request.prompt}],
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.content:
yield f"data: {chunk.choices[0].delta.content}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
# Lambda: streaming NO es nativo
# Necesitarías Lambda function URLs con response streaming (limitado)
# o API Gateway WebSocket API (complejo y costoso)
Cost Crossover Analysis
¿A qué volumen de tráfico Lambda deja de ser más barato que un container en VPS? Esta calculadora te da la respuesta para tu caso específico:
def cost_crossover_analysis(
memory_mb: int = 512,
duration_seconds: float = 2.0,
vps_monthly_cost: float = 24.0,
):
"""Calcula el punto donde Lambda cuesta lo mismo que un VPS."""
lambda_price_per_gb_second = 0.0000166667
lambda_price_per_request = 0.0000002
memory_gb = memory_mb / 1024
cost_per_lambda_request = (
memory_gb * duration_seconds * lambda_price_per_gb_second
) + lambda_price_per_request
crossover_requests = vps_monthly_cost / cost_per_lambda_request
crossover_daily = crossover_requests / 30
print(f"=== Cost Crossover: Lambda vs VPS ===")
print(f"Lambda config: {memory_mb}MB, {duration_seconds}s avg duration")
print(f"VPS cost: ${vps_monthly_cost}/mes")
print(f"Lambda cost per request: ${cost_per_lambda_request:.7f}")
print(f"")
print(f"Crossover: {crossover_requests:,.0f} requests/mes")
print(f" ({crossover_daily:,.0f} requests/día)")
print(f"")
test_volumes = [1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000]
print(f"{'Requests/mes':>15} | {'Lambda':>10} | {'VPS':>10} | {'Ganador':>10}")
print(f"{'-'*15}-+-{'-'*10}-+-{'-'*10}-+-{'-'*10}")
for vol in test_volumes:
lambda_cost = vol * cost_per_lambda_request
winner = "Lambda" if lambda_cost < vps_monthly_cost else "VPS"
print(f"{vol:>15,} | ${lambda_cost:>8.2f} | ${vps_monthly_cost:>8.2f} | {winner:>10}")
# Ejemplo: AI app con 512MB, 2s de duración promedio
cost_crossover_analysis(memory_mb=512, duration_seconds=2.0, vps_monthly_cost=24.0)
Output:
=== Cost Crossover: Lambda vs VPS ===
Lambda config: 512MB, 2.0s avg duration
VPS cost: $24.0/mes
Lambda cost per request: $0.0000169
Crossover: 1,420,118 requests/mes (47,337 requests/día)
Requests/mes | Lambda | VPS | Ganador
---------------+------------+------------+-----------
1,000 | $0.02 | $24.00 | Lambda
10,000 | $0.17 | $24.00 | Lambda
50,000 | $0.84 | $24.00 | Lambda
100,000 | $1.69 | $24.00 | Lambda
500,000 | $8.43 | $24.00 | Lambda
1,000,000 | $16.87 | $24.00 | Lambda
Insight: Para la mayoría de apps AI (que están muy por debajo de 47K req/día), Lambda es más barato en infraestructura pura. Pero recuerda: el coste de debugging y la complejidad operativa de Lambda no aparecen en esta calculadora.
Decision Tree: Serverless vs Containers
¿Tu app necesita streaming (token-by-token)?
├── SÍ → Containers (streaming nativo)
└── NO → ¿Tráfico impredecible (picos grandes)?
├── SÍ → ¿Cold start >3s es aceptable?
│ ├── SÍ → Serverless
│ └── NO → Containers con auto-scaling
└── NO → ¿Presupuesto < $10/mes?
├── SÍ → Serverless (pay-per-use)
└── NO → ¿Necesitas state entre requests?
├── SÍ → Containers (+ Redis/DB)
└── NO → Cualquiera funciona
(elige por preferencia de DX)
Patrón Híbrido: Lo Mejor de Ambos
En producción, muchos equipos usan ambos:
┌──────────────────────────────────┐
│ Container (siempre running) │
│ ├── FastAPI app principal │
│ ├── WebSocket handler │
│ ├── Streaming endpoint │
│ └── Redis (state/cache) │
│ │
│ Serverless (on-demand) │
│ ├── Lambda: procesar documentos │
│ ├── Lambda: generar embeddings │
│ └── Lambda: enviar notificaciones│
└──────────────────────────────────┘
- Container para la API principal: siempre disponible, streaming, stateful
- Serverless para tareas asíncronas: procesamiento batch, eventos, funciones que no necesitan estar siempre running
Cuándo el patrón híbrido tiene sentido
El patrón híbrido no siempre vale la pena. Agrega complejidad (dos sistemas de deployment, dos pipelines de CI/CD, dos sets de monitoring). Úsalo cuando:
- Tienes workloads claramente diferentes: API interactiva (container) + procesamiento batch (Lambda)
- Los costes lo justifican: Si el procesamiento batch es esporádico, Lambda ahorra dinero vs un container idle
- Tu equipo puede mantener ambos: Si eres 1 developer, la complejidad adicional puede no valer la pena
Si toda tu app es una API que llama a un LLM y retorna la respuesta, un solo container es suficiente. No sobreingeniería.
# La API principal (container) delega trabajo a Lambda
import boto3
import json
lambda_client = boto3.client("lambda")
@app.post("/process-document")
async def process_document(file_url: str):
"""API endpoint que delega procesamiento a Lambda."""
lambda_client.invoke(
FunctionName="document-processor",
InvocationType="Event", # Asíncrono
Payload=json.dumps({"file_url": file_url})
)
return {"status": "processing", "message": "Document queued for processing"}
Escenarios AI donde la elección es clara
| Escenario | Elección | Razón en 1 frase |
|---|---|---|
| Chatbot con streaming | Containers | Lambda no soporta SSE/WebSocket nativos |
| Webhook que procesa un evento y llama a GPT | Serverless | Event-driven, stateless, duración corta |
| RAG con ChromaDB de 3GB en memoria | Containers | Necesitas RAM persistente para el vector store |
| Cron job que genera resúmenes diarios | Serverless | Se ejecuta 1 vez/día, paga solo por esa ejecución |
| API de análisis de sentimiento, 100K req/día | Containers | Alto volumen constante, coste fijo más predecible |
| Notificación por email después de inferencia | Serverless | Tarea asíncrona, no necesita estar siempre running |
| Modelo Llama 3 corriendo inferencia local | Containers | Modelo en RAM/GPU, siempre disponible |
Troubleshooting
Problema 1: "Mi Lambda tiene cold starts de >10 segundos"
Causa: Dependencias pesadas (LangChain completo, NumPy, pandas) que se cargan en cada cold start.
Solución: (1) Reduce dependencias: langchain-openai en lugar de langchain[all]. (2) Usa Lambda Layers para pre-empaquetar deps. (3) Si el SLA es estricto, usa Provisioned Concurrency (~$5/mes por instancia warm).
Problema 2: "Mi container consume demasiada RAM"
Causa: ChromaDB, FAISS, u otro vector store cargado completamente en memoria.
Solución: (1) Usa un VPS con más RAM (DigitalOcean 8GB = $48/mes). (2) Evalúa un vector store externo (Pinecone, Qdrant) que no consuma RAM de tu container. (3) Implementa lazy loading: carga solo los índices que necesitas.
Problema 3: "No sé si mi workload es event-driven o always-on"
Causa: No tienes datos de tráfico todavía.
Solución: Si no sabes, empieza con containers (siempre funcionan). Mide tráfico durante 2 semanas. Si ves que el 80% del tiempo no hay requests, evalúa serverless. Es más fácil migrar de container a Lambda que al revés.
Ejercicios Prácticos
Ejercicio 1: Clasifica estos workloads
Para cada workload AI, decide si es mejor serverless o containers:
- Chatbot con streaming de respuestas, 500 usuarios/día
- Procesador de PDFs que extrae datos, se usa 2-3 veces/día
- API de embeddings que mantiene un vector store en memoria
- Servicio de transcripción que procesa archivos de audio subidos por usuarios
- Dashboard AI que responde queries sobre métricas de negocio
Ver solución
-
Chatbot con streaming → Containers. Streaming requiere WebSockets/SSE. Lambda no soporta esto nativamente. FastAPI + uvicorn es la opción clara.
-
Procesador de PDFs, 2-3 veces/día → Serverless. Uso esporádico = pay-per-use ideal. Sin cold start concern (no es tiempo real). Lambda con timeout de 15 min es suficiente para la mayoría de PDFs.
-
API de embeddings con vector store en memoria → Containers. Necesitas RAM persistente para el vector store. Lambda es stateless y tiene memory limits. Un container con suficiente RAM mantiene el store cargado.
-
Transcripción de audio → Serverless. Event-driven (archivo subido → procesar). Lambda + S3 trigger es el patrón perfecto. La transcripción puede tomar minutos pero cabe en el timeout de 15 min.
-
Dashboard AI → Containers. Tráfico predecible (horario laboral), necesita state (sessions, cache de queries), y probablemente websockets para real-time updates.
Ejercicio 2: Calcula el crossover point
Tu app AI tiene 512MB de memoria, 2 segundos de duración promedio por request. ¿A cuántos requests/mes Lambda deja de ser más barato que un VPS de $24/mes?
Ver solución
vps_cost = 24 # $/mes, fijo
lambda_price_per_gb_second = 0.0000166667
lambda_price_per_request = 0.0000002
memory_gb = 512 / 1024 # 0.5 GB
duration_seconds = 2
cost_per_request = (memory_gb * duration_seconds * lambda_price_per_gb_second) + lambda_price_per_request
# = (0.5 * 2 * 0.0000166667) + 0.0000002
# = 0.0000166667 + 0.0000002
# = ~$0.0000169
crossover = vps_cost / cost_per_request
# = 24 / 0.0000169
# = ~1,420,000 requests/mes
# = ~47,000 requests/día
Respuesta: A ~1.4M requests/mes (~47K/día), Lambda iguala al VPS. Por encima de ese volumen, VPS es más barato.
Contexto: La mayoría de apps AI de startups están muy por debajo de 47K req/día, así que Lambda suele ser más barato.
Ejercicio 3: Diseña un sistema híbrido
Tu sistema AI tiene 3 componentes: (a) chat con streaming, (b) procesamiento batch de documentos, (c) API de búsqueda con embeddings. Diseña qué va en containers y qué en serverless.
Ver solución
CONTAINERS (siempre running):
├── (a) Chat API con streaming
│ └── FastAPI + WebSocket, uvicorn
│ └── Razón: streaming nativo, baja latencia
│
├── (c) Search API con embeddings
│ └── FastAPI + FAISS/ChromaDB en memoria
│ └── Razón: vector store en RAM, acceso rápido
│
└── Redis (shared)
└── Cache de responses, session state
SERVERLESS (on-demand):
└── (b) Document processor
└── Lambda triggered por S3 upload
└── Razón: uso esporádico, event-driven, no necesita estado
Arquitectura:
User → API Gateway/Nginx
├── /chat/* → Container (streaming)
├── /search/* → Container (embeddings)
└── /upload → S3 → Lambda trigger → procesamiento
Ejercicio 4: Usa la calculadora de crossover
Modifica la función cost_crossover_analysis de esta cápsula para tu caso: tu app usa 1024MB de memoria y tiene duración promedio de 4 segundos (RAG con múltiples pasos). ¿A cuántos requests/día Lambda iguala a un VPS de $48/mes?
Ver solución
cost_crossover_analysis(memory_mb=1024, duration_seconds=4.0, vps_monthly_cost=48.0)
# Output:
# Lambda config: 1024MB, 4.0s avg duration
# Lambda cost per request: $0.0000669
# Crossover: ~717,000 requests/mes (~23,900 requests/día)
#
# Con más memoria y más duración, el crossover baja.
# A 1024MB y 4s, Lambda supera al VPS a ~24K req/día
# (vs ~47K req/día con 512MB y 2s)
Conclusión: Workloads más pesados (más memoria, más duración) tienen un crossover más bajo. Si tu app RAG es pesada, Lambda se vuelve más caro más rápido.
Ejercicio 5: Cold start mitigation plan
Tu Lambda AI tiene cold starts de 8 segundos (LangChain + embeddings). Tu SLA dice <3s de latencia. Propón 3 estrategias de mitigación.
Ver solución
-
Provisioned Concurrency: Mantener N instancias Lambda siempre warm. Coste: ~$0.0000041667/GB-segundo idle + invocación normal. Para 2 instancias × 512MB: ~$5/mes adicionales. Elimina cold start completamente.
-
Package optimization: Reducir dependencias. ¿Realmente necesitas LangChain completo?
langchain-openai(30MB) vslangchain[all](200MB). Reemplazar imports pesados con alternativas ligeras. Puede reducir cold start de 8s a 3-5s. -
Keep-warm scheduler: CloudWatch Events trigger cada 5 minutos para mantener Lambda warm. Coste: ~288 invocaciones/día × $0.000017 = $0.15/mes. No garantiza warm (AWS puede destruir el container), pero reduce frecuencia de cold starts.
Recomendación: Si el SLA es <3s y el tráfico justifica el coste, provisioned concurrency. Si no, considera migrar a container.
Resumen
- La decisión serverless vs containers es la más frecuente para AI Engineers.
- Containers ganan cuando necesitas: streaming, state, RAM para modelos, debugging directo, latencia consistente.
- Serverless gana cuando: tráfico variable/bajo, procesamiento event-driven, zero ops, presupuesto mínimo.
- Cold starts son el factor AI-specific más importante de serverless: de 1s (SDK ligero) a 25s (modelo en memoria).
- Streaming es un deal-breaker: si necesitas token-by-token, containers.
- El patrón híbrido combina lo mejor: containers para la API principal, serverless para tareas batch/asíncronas.
- El crossover de coste depende de tu volumen: Lambda es más barato hasta ~1.4M req/mes para un workload típico AI.
Recursos Adicionales
- AWS Lambda vs ECS — Choosing the Right Service — Guía de decisión oficial AWS
- Lambda Container Images — Deploy Lambda como container
- FastAPI Streaming Responses — Streaming en FastAPI
- Lambda Power Tuning — Optimizar memoria/coste de Lambda
- Serverless Land — Patrones serverless de AWS
- Docker for AI/ML Workloads — Docker para aplicaciones AI