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:

  1. Containers (Docker/Docker Compose) — Usados para local, staging, y producción en VPS o managed platforms
  2. 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

VentajaPor qué importa para AI
Sin cold startsLa primera request es tan rápida como la milésima
Sin timeoutPrompt chains de minutos sin límite artificial
RAM ilimitada*Embeddings en memoria, modelos locales
Debugging directodocker exec, logs en tiempo real
Streaming nativoSSE/WebSockets para respuestas token-by-token
Entorno consistenteMismo container en dev, staging, prod

*Limitado por el hardware del host

Desventajas para AI

DesventajaImpacto
Coste fijoPagas 24/7 aunque nadie use la app
Escala manualTú decides cuándo agregar más containers
MantenimientoUpdates del OS, Docker, dependencias
NetworkingConfiguras 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

VentajaPor qué importa para AI
Escala a 0No pagas cuando nadie usa la app
Escala automáticaDe 1 a 1000 invocaciones concurrentes sin config
Pay-per-useSolo pagas por tiempo de ejecución real
Zero opsAWS gestiona infra, patches, seguridad
Event-drivenPerfecto para procesamiento asíncrono (archivos, colas)

Desventajas para AI

DesventajaImpacto en AI
Cold starts1-15s para la primera invocación con dependencias AI
Timeout 15minPrompt chains largas pueden exceder el límite
Memory 10GB maxNo puedes cargar modelos grandes en memoria
Sin streamingNo hay WebSockets ni SSE nativos en Lambda
Debugging difícilCloudWatch logs, no puedes SSH al entorno
StatelessCada invocación es independiente, no hay memoria entre requests

Comparación Directa

Tabla detallada

AspectoContainersServerless (Lambda)
Latencia primera request~50-200ms1-15s (cold start)
Latencia request subsecuente~50-200ms~50-200ms (warm)
TimeoutIlimitado15 minutos max
MemoriaLimitado por host128MB-10GB
Streaming (SSE/WS)✅ Nativo❌ No soportado
ConcurrenciaConfig manualAuto (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
Debuggingdocker exec, logs localesCloudWatch, X-Ray
Deploydocker compose up / git pushserverless 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:

  1. Tienes workloads claramente diferentes: API interactiva (container) + procesamiento batch (Lambda)
  2. Los costes lo justifican: Si el procesamiento batch es esporádico, Lambda ahorra dinero vs un container idle
  3. 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

EscenarioElecciónRazón en 1 frase
Chatbot con streamingContainersLambda no soporta SSE/WebSocket nativos
Webhook que procesa un evento y llama a GPTServerlessEvent-driven, stateless, duración corta
RAG con ChromaDB de 3GB en memoriaContainersNecesitas RAM persistente para el vector store
Cron job que genera resúmenes diariosServerlessSe ejecuta 1 vez/día, paga solo por esa ejecución
API de análisis de sentimiento, 100K req/díaContainersAlto volumen constante, coste fijo más predecible
Notificación por email después de inferenciaServerlessTarea asíncrona, no necesita estar siempre running
Modelo Llama 3 corriendo inferencia localContainersModelo 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:

  1. Chatbot con streaming de respuestas, 500 usuarios/día
  2. Procesador de PDFs que extrae datos, se usa 2-3 veces/día
  3. API de embeddings que mantiene un vector store en memoria
  4. Servicio de transcripción que procesa archivos de audio subidos por usuarios
  5. Dashboard AI que responde queries sobre métricas de negocio
Ver solución
  1. Chatbot con streaming → Containers. Streaming requiere WebSockets/SSE. Lambda no soporta esto nativamente. FastAPI + uvicorn es la opción clara.

  2. 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.

  3. 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.

  4. 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.

  5. 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
  1. 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.

  2. Package optimization: Reducir dependencias. ¿Realmente necesitas LangChain completo? langchain-openai (30MB) vs langchain[all] (200MB). Reemplazar imports pesados con alternativas ligeras. Puede reducir cold start de 8s a 3-5s.

  3. 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

  1. AWS Lambda vs ECS — Choosing the Right Service — Guía de decisión oficial AWS
  2. Lambda Container Images — Deploy Lambda como container
  3. FastAPI Streaming Responses — Streaming en FastAPI
  4. Lambda Power Tuning — Optimizar memoria/coste de Lambda
  5. Serverless Land — Patrones serverless de AWS
  6. Docker for AI/ML Workloads — Docker para aplicaciones AI