El año pasado mi equipo — éramos cinco ingenieros en ese momento — llevaba casi dos años con toda la infraestructura de backend en AWS Lambda. Funciones Python para procesar eventos, APIs síncronas, pipelines de datos. Todo serverless. Estábamos orgullosos de eso.
Entonces empezamos a integrar modelos de ML propios. Y todo empezó a crujir.
Este post no es un benchmark neutral. Es cómo tomamos la decisión, qué nos sorprendió en el camino, y qué haría diferente si empezara desde cero en 2026.
Dos Años en Lambda: Lo que Funcionó y el Límite que No Vi Venir
Antes de hablar de contenedores, tengo que ser honesto sobre lo bueno. Lambda funcionó muy bien durante bastante tiempo. Procesábamos unos 2-3 millones de eventos diarios provenientes de webhooks de tiendas — Shopify principalmente — y el modelo de pago por ejecución nos ahorraba mucho comparado con servidores corriendo 24/7 con cargas variables. En los meses de baja temporada, la factura de compute caía casi a cero. Eso es real y no hay que subestimarlo.
El problema llegó cuando empezamos a correr modelos de embeddings para recomendaciones de producto. Nuestro primer modelo de sentence-transformers pesaba unos 420MB solo el checkpoint. Lambda tiene un límite de 250MB para el paquete de deployment sin comprimir, y aunque podés cargar modelos desde S3 al iniciar, eso disparaba los cold starts a entre 8 y 12 segundos. Para una API síncrona, eso es inaceptable.
Intenté workarounds. Cargué el modelo de forma lazy, exploré Lambda SnapStart (tuvimos que reescribir parte del pipeline, no valió la pena), probé contenedores de Lambda que permiten hasta 10GB de imagen. Eso último ayudó un poco, pero el cold start seguía siendo entre 3 y 5 segundos para el modelo grande. Ninguna de las tres opciones era satisfactoria — y lo peor era que cada workaround generaba su propia deuda técnica.
El patrón que me tardó en ver: Lambda sigue siendo excelente para cargas event-driven con dependencias ligeras. En cuanto metés ML o cualquier proceso que requiera estado caliente persistente en memoria, empezás a pelear contra la plataforma en lugar de con ella.
Donde los Contenedores Ganaron la Discusión Interna
La migración a ECS Fargate para las cargas de ML no fue una decisión feliz. Fue una decisión forzada.
Lo primero que noté al mover los pipelines de inferencia a Fargate fue el control. Aunque — déjame retroceder un segundo, porque Fargate no es simple. Tuve que escribir task definitions, ajustar los límites de CPU y memoria, configurar IAM roles específicos, y entender cómo funcionan los scaling policies en detalle. Eso tomó tiempo. Pero una vez listo, tener un contenedor con Python 3.12, CUDA, y todas las dependencias ML corriendo sin restricciones artificiales de tamaño era... alivio, honestamente.
Acá va un ejemplo simplificado de cómo pasamos de una Lambda con carga de modelo a un servicio en Fargate:
# Antes: Lambda handler con cold start doloroso en cada invocación fría
import json
from sentence_transformers import SentenceTransformer
# Se ejecuta en cada cold start — entre 8-12s con el modelo grande
model = SentenceTransformer('all-mpnet-base-v2') # 420MB en disco
def handler(event, context):
texts = [record['body'] for record in event['Records']]
embeddings = model.encode(texts)
# guardar embeddings en DynamoDB...
return {'statusCode': 200}
# Después: servicio FastAPI en Fargate — modelo cargado una vez, persiste en memoria
from fastapi import FastAPI
from sentence_transformers import SentenceTransformer
import uvicorn
app = FastAPI()
# Se carga una vez cuando arranca el contenedor, no en cada request
model = SentenceTransformer('all-mpnet-base-v2')
@app.post("/embeddings")
async def generate_embeddings(texts: list[str]):
# batch_size=32 para aprovechar paralelismo del modelo en memoria
embeddings = model.encode(texts, batch_size=32)
return {"embeddings": embeddings.tolist()}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8080)
La diferencia en latencia fue brutal. P99 bajó de ~9s a ~180ms para el mismo modelo en los mismos requests. El modelo cargado una vez en memoria versus recargado en cada cold start es una diferencia que parece obvia en retrospectiva, pero cuesta verla cuando llevás años pensando en términos de funciones stateless.
Lo que no me gustó de Fargate: el costo base. Con Lambda pagás exactamente lo que usás. Con Fargate, si tenés un task corriendo 24/7 para mantener el modelo caliente en memoria, pagás por esas horas aunque el tráfico sea mínimo a las 3am. Para nuestro workload procesando unos 50k requests por día con picos horarios marcados, el costo mensual en Fargate fue entre 3x y 4x más caro que lo que habríamos pagado con Lambda sin el problema del cold start.
Right, so — ¿cómo justificamos el gasto? Rendimiento medible. Nuestros clientes notaban la diferencia y los datos lo confirmaron: conversion rate en las páginas de recomendaciones subió un 12% cuando la latencia bajó a menos de 200ms. Eso cerró la discusión interna.
Takeaway práctico: Fargate tiene sentido cuando necesitás estado caliente en memoria, dependencias pesadas, o procesos de más de 15 minutos. El costo base es real — hay que justificarlo con impacto concreto de negocio, no con argumentos técnicos.
El Hallazgo que Me Confundió Durante Semanas: Cloud Run
Un colega me recomendó Google Cloud Run para un servicio nuevo — una API de procesamiento de imágenes que necesitaba OpenCV y algo de lógica custom. Cloud Run básicamente te permite correr contenedores Docker de forma serverless: escala a cero cuando no hay tráfico, pero al llegar requests escala contenedores completos.
Mi primera reacción fue: "¿esto no es simplemente Lambda pero con Docker?" Y parcialmente sí. Pero la diferencia práctica me tomó tiempo entender, y creo que la confusión viene de que la distinción no es obvia en la documentación.
Con Lambda container images, vos definís tu imagen pero Lambda gestiona el runtime con sus propias restricciones: timeouts máximos de 15 minutos, el modelo de ejecución de funciones, limitaciones de concurrencia que requieren configuración explícita. Con Cloud Run, el contrato es distinto: vos exponés un servidor HTTP y Cloud Run gestiona cuántas instancias corren. Tu código puede tener estado dentro de una instancia mientras esté viva. Podés usar WebSockets. No existe límite de 15 minutos por request.
Probé Cloud Run para ese servicio de imágenes y el cold start fue de aproximadamente 600-900ms con una imagen de ~1.2GB. No tan rápido como un contenedor siempre encendido, pero mucho más barato en cargas variables. Lo que más me sorprendió fue el pricing model: Cloud Run cobra por tiempo de CPU y memoria durante el procesamiento de requests, no por tiempo de instancia activa (a menos que configures min-instances > 0). Para cargas intermitentes medianas, eso puede ser significativamente más barato que Fargate.
No lo probé a más de 800-1000 requests por segundo concurrentes, así que no puedo hablar de ese rango con seguridad. Pero para nuestro caso, Cloud Run resolvió el equilibrio entre costo y performance de una forma que ni Lambda pura ni Fargate podían ofrecer por separado.
Takeaway práctico: Si estás viendo serverless y contenedores como dos mundos completamente separados, te estás perdiendo opciones intermedias que en 2026 están bastante maduras. Cloud Run y AWS App Runner son probablemente el punto de partida correcto para más proyectos de los que la gente considera.
Los Números Reales Comparados (el Ejercicio que Me Pidió el CFO)
Armé esto después de que me pidieran justificar una factura de AWS que había subido un 40% en tres meses. Empujé esa migración de infra un viernes por la tarde — error clásico — y el proceso de validación de costos duró dos semanas. En retrospectiva, debería haberlo hecho con más datos antes de mover nada.
Tres workloads representativos, costos aproximados mensuales en us-east-1:
Workload A: Procesamiento de webhooks (2M eventos/día, avg 200ms de ejecución)
Lambda: ~$45/mes. Fargate (1 task 0.5 vCPU, 1GB RAM, 24/7): ~$22/mes. Cloud Run: ~$31/mes.
Fargate ganó. Carga constante y predecible significa que el compute dedicado sale más barato que pagar por invocación.
Workload B: API de inferencia ML (50k requests/día, distribución horaria con picos)
Lambda con cold starts: técnicamente no viable para nuestro SLA de latencia bajo 500ms. Fargate (1 task caliente, 2 vCPU, 4GB RAM): ~$110/mes. Cloud Run (1 vCPU, 2GB RAM, min-instances=1 para evitar cold starts): ~$62/mes.
Cloud Run con una instancia mínima ganó. Mantenés el modelo caliente sin pagar por N contenedores inactivos.
Workload C: ETL nocturno (30 minutos por noche, procesamiento intensivo)
Lambda: ~$2/mes. Fargate on-demand: ~$8/mes. Cloud Run: ~$3.50/mes.
Lambda ganó fácil. Para trabajos cortos e infrecuentes, el modelo de pago por ejecución es imbatible.
Lo que estos números muestran, más allá de los valores específicos: el patrón de tráfico importa tanto como el tipo de workload. No existe una respuesta universal.
Mi Recomendación Real Según el Tipo de Proyecto
Voy a ser directo porque "depende de tu caso de uso" no le sirve a nadie cuando está tomando una decisión concreta con fecha límite.
Serverless puro (Lambda, Cloud Functions) es la elección correcta si tu equipo tiene menos de 5 ingenieros de backend, tus workloads son principalmente event-driven con dependencias ligeras (menos de 100MB), y la variabilidad de tráfico es alta o impredecible. El overhead operativo de gestionar contenedores no vale la pena si podés evitarlo.
Contenedores dedicados (Fargate, GKE, EKS) cuando tenés ML en producción con modelos que pesan más de 500MB, procesos que corren por más de 15 minutos, o carga base predecible y sostenida que justifique instancias dedicadas. También si tu equipo ya tiene expertise sólido en Docker y Kubernetes — la curva de aprendizaje ya está amortizada y el beneficio operativo compensa.
La opción intermedia (Cloud Run, App Runner, Azure Container Apps) es donde yo comenzaría hoy para la mayoría de APIs medianas nuevas. Contenedores con billing serverless. La limitación principal es el vendor lock-in a la plataforma de cloud, que puede o no importarte según tu estrategia — aunque honestamente, para equipos de menos de 10 personas construyendo en un solo cloud, ese lock-in raramente es el problema real.
Kubernetes gestionado lo dejaría para equipos con platform engineering dedicado. Con cinco personas, no podíamos darnos ese lujo. Aprendimos eso de la manera cara, no de un post.
Mi veredicto después de este año: para un equipo de 4-6 personas en 2026, el punto de partida debería ser Cloud Run o App Runner, con Lambda reservado para event processing liviano y contenedores dedicados únicamente para workloads de ML con estado o procesamiento intensivo. La arquitectura puramente serverless que teníamos era elegante, pero nos limitó cuando escalamos en complejidad — no en tráfico. Esa es una distinción que no aparece en ningún comparison chart.
Top comments (0)