Arquitectura Serverless
Decide cuándo usar serverless y cuándo no. Cubre patrones FaaS, diseño event-driven, cold starts, modelos de costo, vendor lock-in y estrategias de migración para aplicaciones serverless en producción.
Introducción
Escalado infinito, cero mantenimiento, paga solo por lo que uses — así se vende serverless. Brilla en algunos workloads y se desarma en otros. Si la eliges por las razones equivocadas, te llegan facturas sorpresa, latencia de cold start y sesiones de debugging que nadie disfruta. Dedícale unos minutos y saldrás sabiendo si serverless encaja con tu carga de trabajo, además de qué hace falta para mantenerla corriendo cuando llegue a producción.
Qué Significa Realmente Serverless
Serverless no significa “sin servidores.” Significa que no los gestionas tú. Provisioning, escalado, patching, disponibilidad — eso ahora es trabajo del proveedor. Tu trabajo se reduce a escribir funciones y conectar triggers.
Tradicional: Tu gestionas VMs → Tu deployas app → Tu escalas → Tu patcheas
Contenedor: Tu builds images → Orchestrator deploya → Tu configuras escalado
Serverless: Tu escribes funciones → Provider maneja todo lo demas
Características Clave
- Sin gestión de servidores: el proveedor parchea el OS y aprovisiona la capacidad
- Auto-escalado: pasa de 0 a miles de ejecuciones concurrentes por su cuenta
- Pay-per-use: se factura por tiempo de ejecución y memoria, nunca por tiempo idle
- Event-driven: las funciones se disparan con HTTP requests, mensajes de cola, timers o eventos cloud
- Stateless: cada invocación es independiente — nada sobrevive en memoria entre llamadas
Cuándo Ir Serverless
Buen Fit: Workloads Event-Driven
# AWS Lambda triggerada por S3 upload
import json
import boto3
def lambda_handler(event, context):
for record in event["Records"]:
bucket = record["s3"]["bucket"]["name"]
key = record["s3"]["object"]["key"]
# Procesar archivo subido
process_file(bucket, key)
return {"statusCode": 200, "body": json.dumps({"processed": len(event["Records"])})}
Serverless brilla para workloads que son:
- Intermitentes: El tráfico sube y baja impredeciblemente
- Event-driven: Triggerados por uploads, messages, o webhooks
- Short-lived: Cada ejecución se completa en segundos, no horas
- Stateless: Sin necesidad de conexiones long-running o estado en memoria
Buen Fit: HTTP APIs con Tráfico Variable
# API Gateway + Lambda para endpoints REST
def lambda_handler(event, context):
http_method = event["httpMethod"]
path = event["path"]
if path == "/users" and http_method == "GET":
users = db.users.list(limit=100)
return {"statusCode": 200, "body": json.dumps(users)}
if path == "/users" and http_method == "POST":
body = json.loads(event["body"])
user = db.users.create(body)
return {"statusCode": 201, "body": json.dumps(user)}
return {"statusCode": 404, "body": json.dumps({"error": "Not found"})}
Buen Fit: Tareas Programadas
# CloudWatch Events + Lambda para cron jobs
def lambda_handler(event, context):
# Ejecutar limpieza nocturna
delete_expired_sessions()
send_daily_reports()
update_aggregates()
return {"status": "complete"}
Buen Fit: Stream Processing
# Lambda procesando DynamoDB streams
def lambda_handler(event, context):
for record in event["Records"]:
if record["eventName"] == "INSERT":
new_item = record["dynamodb"]["NewImage"]
# Enviar email de bienvenida para nuevos usuarios
if new_item.get("type", {}).get("S") == "user":
send_welcome_email(new_item["email"]["S"])
return {"status": "processed"}
Cuándo NO Ir Serverless
Mal Fit: Tareas Long-Running
AWS Lambda tiene un timeout de 15 minutos. Todo lo que dure más — video encoding, procesamiento de datos pesado — pertenece a contenedores o VMs.
Mal Fit: Aplicaciones en Tiempo Real
WebSockets, streaming y long-polling necesitan conexiones que se mantienen abiertas. Las funciones son stateless por diseño y viven segundos como mucho. Servidores dedicados o un servicio WebSocket gestionado encajan mejor.
Mal Fit: High-Performance Computing
Lambda llega hasta 10GB de RAM, y la CPU que obtienes escala con la memoria que compras. ML training, simulaciones, cualquier cosa intensiva en CPU — ve por instancias GPU o un cluster HPC.
Mal Fit: Tráfico Constante y Predecible
Con tráfico estable 24/7, serverless sale más caro que instancias reservadas. Contenedores con autoscaling entregan la misma elasticidad por menos.
Mal Fit: Workflows Stateful Complejos
Las funciones serverless son stateless. Los shopping carts o los formularios multi-paso necesitan estado en algún lado — y ese “algún lado” es una base de datos o Redis, más la latencia y complejidad que trae consigo.
Patrones de Diseño Serverless
Patrones de Trigger
La mayoría de las funciones serverless encajan en una de cinco formas de trigger. Elegir la correcta determina cómo escala la función y cómo se manifiestan los fallos.
| Patrón | Caso de uso | Ejemplo |
|---|---|---|
| Función por endpoint HTTP | APIs REST | API Gateway → Lambda |
| Función event-driven | Procesamiento asíncrono | Subida a S3 → generador de thumbnails |
| Función programada | Jobs cron | CloudWatch Events → reporte nocturno |
| Función disparada por cola | Trabajos desacoplados | SQS → procesador de órdenes |
| Función disparada por stream | Datos en tiempo real | DynamoDB Streams → actualizador de caché |
Composición de Funciones
Divide operaciones complejas en funciones más pequeñas que se encadenan.
# Step Functions state machine para procesamiento de ordenes
{
"StartAt": "ValidateOrder",
"States": {
"ValidateOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123:function:validate-order",
"Next": "ChargePayment"
},
"ChargePayment": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123:function:charge-payment",
"Next": "CheckResult"
},
"CheckResult": {
"Type": "Choice",
"Choices": [
{"Variable": "$.paymentStatus", "StringEquals": "success", "Next": "ShipOrder"},
{"Variable": "$.paymentStatus", "StringEquals": "failed", "Next": "NotifyFailure"}
]
},
"ShipOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123:function:ship-order",
"End": true
},
"NotifyFailure": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123:function:notify-failure",
"End": true
}
}
}
Fan-Out / Fan-In
Una función triggera múltiples funciones paralelas, luego agrega resultados.
import boto3
import json
from concurrent.futures import ThreadPoolExecutor
lambda_client = boto3.client("lambda")
def fan_out_handler(event, context):
items = event["items"]
# Invocar processor para cada item en paralelo
with ThreadPoolExecutor(max_workers=10) as executor:
futures = []
for item in items:
futures.append(
executor.submit(
lambda_client.invoke,
FunctionName="process-item",
Payload=json.dumps({"item": item})
)
)
results = [json.loads(f.result()["Payload"].read()) for f in futures]
# Agregar resultados
return {"processed": len(results), "results": results}
Event Sourcing
Trata los eventos almacenados como la fuente de verdad, y deja que las funciones los proyecten en read models.
# Funcion 1: Guardar event
def save_event_handler(event, context):
event_store.append({
"aggregate_id": event["aggregateId"],
"event_type": event["type"],
"data": event["data"],
"timestamp": event["timestamp"]
})
return {"status": "saved"}
# Funcion 2: Proyectar a read model (triggerada por event store)
def project_handler(event, context):
for record in event["Records"]:
event_data = json.loads(record["body"])
if event_data["event_type"] == "UserCreated":
db.users.upsert({
"id": event_data["aggregate_id"],
"name": event_data["data"]["name"],
"email": event_data["data"]["email"]
})
Sagas y Transacciones de Compensación
Las funciones no pueden mantener transacciones ACID distribuidas entre servicios. Cuando una operación de negocio abarca varias funciones, usa el patrón saga: cada función ejecuta su paso y publica un evento, y si un paso falla, una función de compensación deshace el trabajo anterior. Step Functions expresa la compensación con bloques Catch que enrutan los fallos a un estado de rollback.
# Saga de procesamiento de órdenes — un fallo de pago dispara la compensación
Comment: Saga de procesamiento de órdenes
StartAt: ValidateOrder
States:
ValidateOrder:
Type: Task
Resource: arn:aws:lambda:us-east-1:123:function:validate-order
Next: ProcessPayment
ProcessPayment:
Type: Task
Resource: arn:aws:lambda:us-east-1:123:function:process-payment
Catch:
- ErrorEquals: ["PaymentFailed"]
ResultPath: "$.error"
Next: RefundOrder
Next: NotifySuccess
RefundOrder:
Type: Task
Resource: arn:aws:lambda:us-east-1:123:function:refund-order
End: true
NotifySuccess:
Type: Task
Resource: arn:aws:lambda:us-east-1:123:function:notify-success
End: true
Para consistencia dentro de un solo servicio, el patrón outbox es más simple: la función escribe en la base de datos y registra el evento en la misma transacción, y luego un proceso separado lee la tabla outbox y publica los eventos.
Cold Starts
Un cold start ocurre cuando nadie ha llamado a la función recientemente: el proveedor tiene que levantar un contenedor nuevo y cargar el runtime antes de que tu código pueda ejecutarse — lo que cuesta entre 1 y 10 segundos de entrada.
Factores de Cold Start
| Factor | Impacto | Mitigacion |
|---|---|---|
| Runtime | Java > .NET > Python > Node.js > Go | Elegir runtimes de inicio rápido (Go, Node.js) |
| Tamaño de paquete | Más grande = cold start más lento | Minimizar dependencias |
| Configuracion VPC | VPC añade 1-2s cold start | Usar VPC-less si es posible |
| Asignación de memoria | Más memoria = inicio más rápido | Asignar 512MB+ para funciones CPU-heavy |
| Provisioned concurrency | Elimina cold start | Pagar por instancias always-warm |
Mitigando Cold Starts
# Lazy initialization: cargar dependencias pesadas fuera del handler
import boto3
import json
# Estos cargan solo en cold start
s3 = boto3.client("s3")
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("users")
def lambda_handler(event, context):
# Handler es rapido — dependencias ya cargadas
user_id = event["pathParameters"]["userId"]
response = table.get_item(Key={"id": user_id})
return {
"statusCode": 200,
"body": json.dumps(response.get("Item", {}))
}
Provisioned Concurrency
Los endpoints sensibles a latencia pueden pagar provisioned concurrency, que mantiene instancias warm.
# AWS CLI: asignar provisioned concurrency
aws lambda put-provisioned-concurrency-config \
--function-name my-api \
--qualifier live \
--provisioned-concurrent-executions 10
Otras opciones que conviene conocer: los pings keep-alive programados eliminan el cold start en funciones invocadas raramente a un costo casi nulo, y compilar a código nativo (Go, Rust, o AOT de .NET) recorta el tiempo de inicialización frente a runtimes interpretados. Para un recorrido práctico, consulta Optimizar Cold Starts en AWS Lambda.
Modelo de Costo
Cómo Funciona el Pricing Serverless
Costo = (Numero de requests) × (Duracion de ejecucion en ms) × (Memoria asignada) × (Precio por GB-ms)
Pricing de AWS Lambda (ejemplo):
- $0.20 por 1 millón de requests
- $0.0000166667 por GB-segundo
- Free tier: 1 millón de requests + 400,000 GB-segundos por mes
Ejemplo de Comparación de Costo
# Escenario: 10 millones de requests/mes, 100ms cada uno, 512MB memoria
# Serverless (Lambda)
requests_cost = 10_000_000 / 1_000_000 * 0.20 # $2.00
compute_cost = 10_000_000 * 0.1 * 0.5 / 1024 * 0.0000166667 * 1000 # ~$8.13
total_serverless = requests_cost + compute_cost # ~$10.13/mes
# Contenedor (ECS Fargate: 0.5 vCPU, 1GB RAM, always running)
container_cost = 730 * (0.025 * 0.5 + 0.003 * 1) # ~$10.22/mes
# Break-even: ~10M requests/mes a 100ms cada uno
# Menos de 10M: serverless es mas barato
# Más de 10M: contenedor es más barato
Tips de Optimización de Costo
- Right-size memory: Más memoria puede reducir el tiempo de ejecución, bajando el costo total
- Reducir tiempo de ejecución: Optimizar código para terminar más rápido
- Usar caching: Cachear computaciones costosas en Redis o DynamoDB DAX
- Evitar over-provisioning: Empezar con 128MB y aumentar solo si es necesario
- Monitorear costos: Setear billing alerts para detectar spikes inesperados
Vendor Lock-In
Las plataformas serverless difieren más de lo que parece. Mover de Lambda a Cloud Functions o Azure Functions implica reescribir handlers, triggers y configuración de infraestructura.
Factores de Lock-In
| Factor | AWS Lambda | Google Cloud Functions | Azure Functions |
|---|---|---|---|
| Handler signature | lambda_handler(event, context) | handler(request) | main(req: func.HttpRequest) |
| Triggers | API Gateway, S3, SQS, DynamoDB | HTTP, Pub/Sub, Cloud Storage | HTTP, Blob, Queue, Event Grid |
| Infra as Code | CloudFormation, SAM, CDK | Cloud Deployment Manager | ARM, Bicep |
| Gestión de estado | Step Functions | Workflows | Durable Functions |
Reduciendo Lock-In
# Abstraer el handler de la logica de negocio
def process_order(order_data: dict) -> dict:
# Logica de negocio pura — sin codigo cloud-specific
validate(order_data)
charge_payment(order_data)
ship_order(order_data)
return {"status": "complete"}
# AWS Lambda handler
def lambda_handler(event, context):
order_data = json.loads(event["body"])
result = process_order(order_data)
return {"statusCode": 200, "body": json.dumps(result)}
# Google Cloud Functions handler
def handler(request):
order_data = request.get_json()
result = process_order(order_data)
return json.dumps(result)
Estrategia de Migración
Patrón Strangler Fig
Reemplaza gradualmente un monolito con funciones serverless. Enruta el tráfico hacia serverless primero para las features nuevas, y luego reemplaza progresivamente la funcionalidad existente — consulta el patrón strangler fig para la mecánica completa de migración.
# API Gateway: rutar nuevos endpoints a Lambda, viejos a monolito
{
"routes": [
{"path": "/api/v2/users", "target": "lambda:users-v2"},
{"path": "/api/v1/*", "target": "http://monolith.example.com"}
]
}
Pasos de Migración
- Identificar candidatos: elige endpoints intermitentes, event-driven o stateless
- Empezar pequeño: mueve un endpoint a serverless y obsérvalo
- Testear exhaustivamente: verifica performance, costo y confiabilidad antes de seguir
- Rutar tráfico: deja que API Gateway divida los requests entre lo viejo y lo nuevo
- Monitorear: compara métricas en ambos lados
- Expandir: migra más endpoints a medida que crece la confianza
- Descommissionar: apaga la infraestructura vieja cuando la migración aterrice
Ejemplo Resuelto: Pipeline de Procesamiento de Imágenes
La teoría solo llega hasta cierto punto. Aquí va un pipeline event-driven completo — subes una imagen a S3 y obtienes tres versiones redimensionadas más un registro de metadata — con la infraestructura, el monitoreo y los números de costo detrás.
Paso 1 — Subida. Alguien deja una imagen en uploads/originals/ y el evento S3:ObjectCreated dispara la función de resize.
Paso 2 — Resize. La función lee la imagen, genera las versiones thumbnail (100×100), medium (500×500) y large (1200×1200) en uploads/resized/, y luego deja un mensaje en image-processed-queue con las claves generadas. Necesita 1024 MB de memoria y un timeout de 30 segundos — las imágenes grandes tardan 10–15 segundos en procesarse.
Paso 3 — Metadata. Una segunda Lambda consume la cola, extrae dimensiones, formato, tamaño y hash, y los escribe en una tabla ImageMetadata (PK imageId, SK versión). El mensaje se elimina al procesar correctamente; tras tres recepciones fallidas termina en image-processed-dlq.
La infraestructura en Terraform:
resource "aws_lambda_function" "resize" {
function_name = "image-resize"
handler = "handler.lambda_handler"
runtime = "python3.11"
memory_size = 1024
timeout = 30
environment {
variables = {
OUTPUT_BUCKET = "uploads-resized"
QUEUE_URL = aws_sqs_queue.image_processed.id
}
}
}
resource "aws_sqs_queue" "image_processed" {
name = "image-processed-queue"
visibility_timeout = 60 # debe superar el timeout de la Lambda
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.dlq.arn
maxReceiveCount = 3
})
}
Ojo con el visibility_timeout de 60 segundos — tiene que superar el timeout de la Lambda, o el mensaje puede reaparecer en la cola mientras la función todavía lo está procesando y terminar manejado dos veces.
Monitoreo. Alarmas de CloudWatch sobre tasa de errores superior al 1% y duración p95 por encima de 20 segundos, una notificación de SNS a Slack cuando algo cae en la DLQ, y tracing con X-Ray para desglosar la latencia por paso.
Fallos parciales de batch. Cuando la Lambda de metadata lee un batch de mensajes SQS y uno envenena el lote, devolver un error genérico hace que SQS reintente todos los mensajes — incluidos los que se procesaron bien. Devuelve batchItemFailures solo con los IDs de los mensajes fallidos, para que el resto se confirme y se elimine.
Costo a 10k imágenes por día: alrededor de $19/mes — unos $15 por Lambda (1.5M de invocaciones), $2 por almacenamiento S3, $0.40 por SQS y $1.25 por DynamoDB on-demand.
Observabilidad
Serverless es donde el debugging viene a sufrir: no hay SSH a una función ni debugger que attachar.
Logging Estructurado
import json
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
request_id = context.aws_request_id
logger.info(json.dumps({
"request_id": request_id,
"event": "function_start",
"user_id": event.get("user_id"),
"action": event.get("action")
}))
try:
result = process_request(event)
logger.info(json.dumps({
"request_id": request_id,
"event": "function_success",
"duration_ms": 150
}))
return result
except Exception as e:
logger.error(json.dumps({
"request_id": request_id,
"event": "function_error",
"error": str(e)
}))
raise
Distributed Tracing
Herramientas de tracing como X-Ray u OpenTelemetry siguen un request mientras salta entre funciones, y capturan automáticamente las llamadas al SDK de AWS en el camino.
from aws_xray_sdk.core import patch_all
import boto3
patch_all()
def lambda_handler(event, context):
# X-Ray automaticamente tracea esta funcion
# y cualquier AWS SDK call que haga
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("users")
response = table.get_item(Key={"id": event["user_id"]})
return response.get("Item")
Seguridad
IAM Least Privilege
Dale a cada función solo los permisos que realmente necesita.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:PutItem"],
"Resource": "arn:aws:dynamodb:us-east-1:123:table/users"
}
]
}
Variables de Entorno para Secrets
No hardcodees secrets — extráelos de Secrets Manager o Parameter Store mediante variables de entorno.
import os
import boto3
def get_secret():
secret_name = os.environ["DB_SECRET_NAME"]
client = boto3.client("secretsmanager")
response = client.get_secret_value(SecretId=secret_name)
return json.loads(response["SecretString"])
Errores Comunes
- Lambda monolítica: meter toda una aplicación en una sola función. Divídela en funciones de propósito único para que los fallos y el escalado queden aislados.
- Espera síncrona: llamar servicios downstream lentos de forma síncrona dentro de una función. Pagas por tiempo de espera inactivo — usa colas o step functions.
- Ignorar los límites de timeout: Lambda tiene un tope de 15 minutos y Azure Functions de 10. Los trabajos más largos pertenecen a ECS, Batch o Fargate.
- Tratar las funciones como servidores: escribir estado en memoria o disco local. Lo que escribas ahí desaparece cuando el contenedor se recicla — persiste externamente.
- Sin estrategia de reintentos: las invocaciones asíncronas que fallan en silencio pierden trabajo. Adjunta siempre una cola de mensajes muertos y una alarma.
- Memoria sobreaprovisionada: en Lambda la memoria también controla la CPU asignada, así que subirla a ciegas puede ayudar — pero mídelo, porque también sube el precio por milisegundo.
Checklist de Producción
- Funciones son stateless e idempotent
- Mitigacion de cold start (provisioned concurrency o lazy init)
- Asignacion de memoria right-sized
- Timeout seteado apropiadamente (no el max por defecto)
- Roles IAM siguen least privilege
- Secrets en Secrets Manager o Parameter Store
- Logging estructurado con request IDs
- Distributed tracing habilitado
- Dead letter queue para invocaciones async fallidas
- Estrategia de retry configurada
- Monitoreo de costo y billing alerts
- Deployment via Infrastructure as Code (SAM, CDK, Serverless Framework)
See Also
- Companion ejecutable — handlers de Lambda, cola SQS + DLQ y Terraform para el pipeline de imágenes
- Runbook de Cold Starts Serverless
- Complete Guide to AWS Lambda in Production
- Orchestrate Serverless Workflows with AWS Step Functions
- Multi-Cloud Strategies — Benefits, Pitfalls
- Complete Guide to Event-Driven Systems
Preguntas frecuentes
¿Es serverless más barato que contenedores?
Depende de la forma de tu tráfico. El tráfico intermitente o con picos favorece a serverless porque nunca pagas por tiempo idle; el tráfico alto y estable favorece a los contenedores, donde la capacidad reservada sale más barata. El break-even suele caer alrededor del 60–70% de utilización sostenida.
¿Cómo manejo tareas long-running en serverless?
Divídelas en piezas más pequeñas con Step Functions o un patrón basado en cola, de modo que cada función quede bajo el límite de timeout. Para trabajos realmente largos medidos en horas, AWS Batch o Fargate es la herramienta correcta.
¿Puedo usar serverless para websockets?
No directamente. La WebSocket API de API Gateway puede mantener la conexión abierta mientras Lambda procesa cada mensaje; opciones gestionadas como AppSync subscriptions o Pusher también sirven.
¿Cómo testeo funciones serverless localmente?
Para un runtime de Lambda local, sam local invoke de la SAM CLI o serverless invoke local emulan el entorno de ejecución. Para integration tests, LocalStack emula los servicios AWS con los que habla tu función.
¿Qué es provisioned concurrency?
Provisioned concurrency precalienta un número fijo de instancias para que respondan al instante. Pagas por esa capacidad se use o no — vale la pena en endpoints sensibles a latencia donde un cold start es inaceptable.
¿Cómo manejo conexiones de base de datos en serverless?
Cada invocación puede abrir una conexión nueva a la base de datos, lo que agota el pool rápido. Pon un proxy delante — Amazon RDS Proxy o pgBouncer. O evita el pooling del todo: DynamoDB no tiene pool de conexiones que agotar, y Aurora Serverless escala las conexiones por su cuenta.
Recursos Relacionados
Estrategias de Caching con Redis
caching con Redis: cache-aside, read-through, write-through, write-behind y refresh-ahead. Cubre politicas de eviction, tuning de TTL, serializacion y operaciones de produccion.
PatternPatrón Circuit Breaker en Python, JavaScript y Java
Previene fallos en cascada deteniendo solicitudes a servicios que están fallando. Un patrón arquitectural para sistemas distribuidos resilientes.
PatternPatrón Strangler Fig
Cómo reemplazar gradualmente un legacy system interceptando routes y routeando traffic a new services. Cubre strangler fig, incremental migration, y cutover.
RecipeOrquestar Workflows Serverless con AWS Step Functions
Construir workflows de state machine con AWS Step Functions usando estados secuenciales, paralelos y map para orquestar funciones Lambda y procesos de larga duracion.
GuideAWS Lambda en Producción
Ejecutar AWS Lambda en produccion con confianza. Cubre optimizacion de cold starts, layers, patrones de deployment, observabilidad con X-Ray, security hardening, connection pooling y cost tuning.
GuideBases de Datos Serverless
Elegir y operar bases de datos serverless para aplicaciones event-driven. Cubre DynamoDB, Aurora Serverless, FaunaDB y PlanetScale con pricing, escalado, patrones de query y estrategias de migracion.