Referencia Detallada de AWS 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.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Introducción
AWS Lambda es la plataforma serverless mas usada. Hacer que una funcion Lambda funcione es facil. Ejecutarla de forma confiable en produccion es dificil. Cold starts, connection pooling, estrategias de deployment, observabilidad, y seguridad todos requieren atencion cuidadosa. Aqui se presenta una guia sobre todo lo que necesitas para ejecutar funciones Lambda en produccion con confianza.
Optimización de Cold Start
Los cold starts son el mayor desafio de performance en Lambda. Cuando una funcion no ha sido invocada recientemente, AWS provisiona un nuevo execution environment, carga el runtime, e inicializa tu codigo. Esto anade 1-10 segundos de latencia.
Medir Cold Starts
import json
import time
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# Trackear tiempo de inicializacion
init_start = time.time()
# Inicializacion pesada fuera del handler
import boto3
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("users")
init_duration = time.time() - init_start
logger.info(json.dumps({"event": "cold_start_init", "duration_ms": round(init_duration * 1000)}))
def lambda_handler(event, context):
start = time.time()
# Tu logica de funcion
user_id = event["pathParameters"]["userId"]
response = table.get_item(Key={"id": user_id})
duration = time.time() - start
logger.info(json.dumps({
"event": "invocation_complete",
"duration_ms": round(duration * 1000),
"request_id": context.aws_request_id
}))
return {
"statusCode": 200,
"body": json.dumps(response.get("Item", {}), default=str)
}
Reducir el Tamaño del Paquete
Los paquetes de deployment grandes ralentizan los cold starts. Minimiza las dependencias.
# Mal: importar libreria completa
import numpy # paquete de 55MB
# Bien: importar solo lo que necesitas
from numpy import array, mean # Todavia carga la libreria completa
# Mejor: usar alternativas ligeras
# En lugar de numpy para matematica simple:
def calculate_average(numbers):
return sum(numbers) / len(numbers)
Lambda Layers
Layers te permiten compartir dependencias across funciones sin bundlearlas en cada paquete de deployment. Esto reduce el tamano del paquete y el tiempo de deployment.
# Crear un layer con dependencias compartidas
mkdir -p python/lib/python3.11/site-packages
pip install requests -t python/lib/python3.11/site-packages/
zip -r requests-layer.zip python/
# Publicar el layer
aws lambda publish-layer-version \
--layer-name requests-layer \
--zip-file fileb://requests-layer.zip \
--compatible-runtimes python3.11
# Usar el layer en tu funcion
# El layer ARN se configura en la lista de layers de la funcion
import requests # Disponible desde el layer
def lambda_handler(event, context):
response = requests.get("https://api.example.com/data")
return {"statusCode": 200, "body": response.text}
Provisioned Concurrency
Para funciones sensibles a latencia, provisioned concurrency mantiene instancias warm.
# Habilitar provisioned concurrency
aws lambda put-provisioned-concurrency-config \
--function-name my-api \
--qualifier live \
--provisioned-concurrent-executions 20
# Nivel aplicacion: trackear si es una invocacion warm o cold
import os
def lambda_handler(event, context):
# Checkear si es cold start
is_cold = os.environ.get("AWS_LAMBDA_INITIALIZATION_TYPE") == "provisioned-concurrency"
if is_cold:
logger.info("Running on provisioned concurrency (sin cold start)")
# Procesar request
return handle_request(event)
Selección de Runtime
| Runtime | Cold Start (promedio) | Mejor Para |
|---|---|---|
| Go | ~100ms | Alta performance, low cold start |
| Node.js | ~200ms | Web APIs, inicio rapido |
| Python | ~300ms | Data processing, ML inference |
| .NET | ~500ms | Enterprise apps |
| Java | ~1000ms | Frameworks pesados (Spring) |
Elije Go o Node.js para endpoints sensibles a latencia. Usa Python para data processing donde los cold starts son menos criticos.
Patrones de Deployment
Deployment Blue/Green
Deploya una nueva version junto a la vieja. Shiftea trafico gradualmente.
# Publicar nueva version
aws lambda publish-version --function-name my-api
# Actualizar alias para apuntar a nueva version (10% canary)
aws lambda update-alias \
--function-name my-api \
--name live \
--function-version 2 \
--routing-config '{"AdditionalVersionWeights": {"1": 0.9}}'
# Rollout completo: shiftear 100% a nueva version
aws lambda update-alias \
--function-name my-api \
--name live \
--function-version 2 \
--routing-config '{}'
Traffic Shifting con CodeDeploy
# appspec.yml para deployment con CodeDeploy
version: 0.0
Resources:
- myLambdaFunction:
Type: AWS::Lambda::Function
Properties:
Name: my-api
Alias: live
CurrentVersion: 1
TargetVersion: 2
DeploymentPreference:
Type: Canary10Percent5Minutes
Infrastructure as Code con SAM
# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./src
Handler: app.handler
Runtime: python3.11
MemorySize: 512
Timeout: 30
Environment:
Variables:
TABLE_NAME: !Ref UsersTable
Policies:
- DynamoDBReadPolicy:
TableName: !Ref UsersTable
Events:
GetUser:
Type: Api
Properties:
Path: /users/{userId}
Method: GET
AutoPublishAlias: live
DeploymentPreference:
Type: Canary10Percent5Minutes
Alarms:
- !Ref ApiErrorAlarm
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ApiErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
MetricName: Errors
Namespace: AWS/Lambda
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 5
ComparisonOperator: GreaterThanThreshold
Connection Pooling
Cada execution environment de Lambda abre su propia conexion de base de datos. Con alta concurrencia, esto agota el connection pool.
El Problema
100 invocaciones concurrentes de Lambda
→ 100 execution environments separados
→ 100 conexiones de base de datos
→ Connection pool agotado (max: 50)
→ Errores de connection refused
Solución: RDS Proxy
RDS Proxy gestiona un connection pool compartido across execution environments de Lambda.
import os
import boto3
import json
# Conectar a traves de RDS Proxy (endpoint es diferente de RDS directo)
def get_db_connection():
proxy_endpoint = os.environ["DB_PROXY_ENDPOINT"]
# RDS Proxy maneja pooling automaticamente
return psycopg2.connect(
host=proxy_endpoint,
dbname=os.environ["DB_NAME"],
user=os.environ["DB_USER"],
password=get_secret()["password"]
)
def lambda_handler(event, context):
conn = get_db_connection()
try:
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = %s", (event["user_id"],))
user = cursor.fetchone()
return {"statusCode": 200, "body": json.dumps(user)}
finally:
cursor.close()
conn.close()
Alternativa: DynamoDB
DynamoDB no usa connection pools. Cada invocacion de Lambda hace una HTTP API call independiente. Esto lo hace ideal para workloads serverless.
import boto3
import json
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("users")
def lambda_handler(event, context):
response = table.get_item(Key={"id": event["user_id"]})
return {
"statusCode": 200,
"body": json.dumps(response.get("Item", {}), default=str)
}
Observabilidad
Logging Estructurado con CloudWatch
import json
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
request_id = context.aws_request_id
function_name = context.function_name
# Loggear event estructurado
logger.info(json.dumps({
"level": "INFO",
"request_id": request_id,
"function": function_name,
"event_type": "request_start",
"user_id": event.get("user_id"),
"http_method": event.get("httpMethod"),
"path": event.get("path")
}))
try:
result = process_request(event)
logger.info(json.dumps({
"level": "INFO",
"request_id": request_id,
"event_type": "request_success",
"duration_ms": 150
}))
return result
except Exception as e:
logger.error(json.dumps({
"level": "ERROR",
"request_id": request_id,
"event_type": "request_error",
"error": str(e),
"error_type": type(e).__name__
}))
return {"statusCode": 500, "body": json.dumps({"error": "Internal error"})}
AWS X-Ray Tracing
from aws_xray_sdk.core import patch_all
import boto3
import json
# Patchear todas las AWS SDK calls para tracing
patch_all()
def lambda_handler(event, context):
# X-Ray tracea esta funcion y todas las AWS SDK calls
user_id = event["user_id"]
# Subsegment para logica custom
from aws_xray_sdk.core import xray_recorder
with xray_recorder.in_subsegment("fetch_user_data"):
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("users")
response = table.get_item(Key={"id": user_id})
with xray_recorder.in_subsegment("transform_data"):
user = response.get("Item", {})
user["full_name"] = f"{user.get('first_name', '')} {user.get('last_name', '')}"
return {"statusCode": 200, "body": json.dumps(user, default=str)}
Metricas Custom con CloudWatch
import boto3
import time
cloudwatch = boto3.client("cloudwatch")
def emit_metric(metric_name, value, unit="Count"):
cloudwatch.put_metric_data(
Namespace="MyApp/Lambda",
MetricData=[{
"MetricName": metric_name,
"Value": value,
"Unit": unit,
"Dimensions": [
{"Name": "Function", "Value": "user-api"}
]
}]
)
def lambda_handler(event, context):
start = time.time()
try:
result = process_request(event)
duration = (time.time() - start) * 1000
emit_metric("RequestCount", 1)
emit_metric("RequestDuration", duration, "Milliseconds")
return result
except Exception as e:
emit_metric("ErrorCount", 1)
raise
Security Hardening
Roles IAM Least Privilege
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:us-east-1:123:table/users"
},
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": "arn:aws:secretsmanager:us-east-1:123:secret:db-credentials-*"
}
]
}
Gestión de Secrets
import os
import json
import boto3
from functools import lru_cache
@lru_cache(maxsize=1)
def get_db_credentials():
"""Cargar credenciales una vez por execution environment."""
client = boto3.client("secretsmanager")
response = client.get_secret_value(
SecretId=os.environ["DB_SECRET_ARN"]
)
return json.loads(response["SecretString"])
def lambda_handler(event, context):
creds = get_db_credentials()
# Credenciales estan cacheadas across invocaciones warm
conn = psycopg2.connect(
host=creds["host"],
dbname=creds["dbname"],
user=creds["username"],
password=creds["password"]
)
# ...
Configuración VPC
Las funciones que acceden instancias RDS privadas deben estar en una VPC. Esto anade latencia de cold start.
# SAM template con configuracion VPC
Resources:
VpcFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./src
Handler: app.handler
Runtime: python3.11
VpcConfig:
SubnetIds:
- subnet-abc123
- subnet-def456
SecurityGroupIds:
- sg-abc123
Policies:
- VPCAccessPolicy: {}
- DynamoDBReadPolicy:
TableName: users
Validación de Input
import json
import re
def validate_email(email):
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
def lambda_handler(event, context):
body = json.loads(event.get("body", "{}"))
# Validar campos requeridos
required = ["email", "name"]
for field in required:
if field not in body:
return {"statusCode": 400, "body": json.dumps({"error": f"Missing field: {field}"})}
# Validar formato de email
if not validate_email(body["email"]):
return {"statusCode": 400, "body": json.dumps({"error": "Invalid email format"})}
# Validar longitud de nombre
if len(body["name"]) > 100:
return {"statusCode": 400, "body": json.dumps({"error": "Name too long"})}
# Procesar request valido
user = create_user(body)
return {"statusCode": 201, "body": json.dumps(user)}
Error Handling y Retries
Funciones Idempotent
Lambda reintenta invocaciones async fallidas. Haz funciones idempotent para manejar procesamiento duplicado.
import hashlib
def lambda_handler(event, context):
# Generar idempotency key del event
event_hash = hashlib.md5(json.dumps(event, sort_keys=True).encode()).hexdigest()
idempotency_key = f"idempotency:{event_hash}"
# Checkear si ya fue procesado
if redis.exists(idempotency_key):
return {"status": "already_processed", "result": redis.get(idempotency_key)}
# Procesar el event
result = process_event(event)
# Guardar resultado con TTL
redis.setex(idempotency_key, 3600, json.dumps(result))
return {"status": "processed", "result": result}
Dead Letter Queues
# SAM template con DLQ
Resources:
FunctionWithDLQ:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./src
Handler: app.handler
Runtime: python3.11
EventInvokeConfig:
MaximumEventAgeInSeconds: 3600
MaximumRetryAttempts: 2
DestinationConfig:
OnFailure:
Destination: !GetAtt FailureQueue.Arn
FailureQueue:
Type: AWS::SQS::Queue
Properties:
MessageRetentionPeriod: 1209600 # 14 dias
Optimización de Costo
Tuning de Memoria
Lambda cobra por memoria × tiempo. Mas memoria tambien significa mas CPU. Encontrar el balance correcto reduce costo.
# Power tuning: testear diferentes configuraciones de memoria
# Usar aws-lambda-power-tuning tool para encontrar memoria optima
# Guias generales:
# 128MB: Operaciones I/O simples
# 256MB: Procesamiento ligero, API handlers
# 512MB: Procesamiento moderado, database queries
# 1024MB: Tareas CPU-intensive
# 3008MB: Maximo CPU para computacion pesada
Evitar Invocaciones Innecesarias
# Batch processing: procesar multiples items por invocacion
def lambda_handler(event, context):
# SQS batch: hasta 10 messages por invocacion
batch_failures = []
for record in event["Records"]:
try:
process_message(json.loads(record["body"]))
except Exception as e:
batch_failures.append({"itemIdentifier": record["messageId"]})
if batch_failures:
return {"batchItemFailures": batch_failures}
return {"batchItemFailures": []}
Checklist de Producción
- Cold start mitigado (lazy init, provisioned concurrency, o layers)
- Tamano de memoria tuned para costo/rendimiento
- Timeout seteado apropiadamente (no default 3s ni max 15m)
- Rol IAM sigue least privilege
- Secrets en Secrets Manager o Parameter Store
- Conexiones de base de datos a traves de RDS Proxy o usando DynamoDB
- Logging estructurado con request IDs
- X-Ray tracing habilitado
- Metricas custom de CloudWatch para KPIs de negocio
- Dead letter queue para invocaciones async
- Funciones son idempotent
- Deployment via SAM/CDK con canary releases
- CloudWatch alarms para errores y duracion
- Validacion de input en todos los endpoints
- Configuracion VPC si accede recursos privados
Preguntas Frecuentes
¿Cómo reduzco los cold starts de Lambda?
Minimiza el tamano del paquete, usa Lambda layers para dependencias compartidas, inicializa recursos pesados fuera del handler, elige runtimes de inicio rapido (Go, Node.js), y usa provisioned concurrency para funciones sensibles a latencia.
¿Debería usar RDS Proxy con Lambda?
Si, si te conectas a RDS desde Lambda. Sin RDS Proxy, la alta concurrencia agota los connection pools de base de datos. RDS Proxy mantiene un connection pool compartido across execution environments de Lambda.
¿Cómo deployo Lambda sin downtime?
Usa alias routing con CodeDeploy. Publica una nueva version, shiftea 10% del trafico a ella, monitorea errores, y completa el rollout. Si los errores exceden el threshold, CodeDeploy automaticamente roll back.
¿Cuál es la concurrencia maxima de Lambda?
El limite por defecto de cuenta es 1,000 ejecuciones concurrentes por region. Puedes solicitar un aumento de quota. Usa reserved concurrency para limitar funciones especificas y prevenir que consuman toda la concurrencia de la cuenta.
¿Cómo manejo tareas long-running?
Lambda tiene un timeout de 15 minutos. Para tareas mas largas, usa Step Functions para encadenar multiples funciones Lambda, o usa AWS Batch/Fargate para workloads verdaderamente long-running.
¿Cómo testeo funciones Lambda localmente?
Usa SAM CLI (sam local invoke, sam local start-api) para testear funciones localmente. Usa LocalStack para emulacion de AWS services. Escribe unit tests para logica de handler e integration tests para interacciones con AWS services.
See Also
Recursos Relacionados
Complete Guide to Serverless Architecture
Decide when to go serverless and when not to. Covers FaaS patterns, event-driven design, cold starts, cost models, vendor lock-in, and migration strategies for production serverless applications.
GuideComplete Guide to Redis Caching Strategies
Master Redis caching with cache-aside, read-through, write-through, write-behind, and refresh-ahead patterns. Covers eviction policies, TTL tuning, serialization, and production operations.
PatternCircuit Breaker Pattern
Prevent cascading failures by stopping requests to failing services. An architectural pattern for resilient distributed systems.
GuideComplete Guide to Serverless Databases
Choose and operate serverless databases for event-driven applications. Covers DynamoDB, Aurora Serverless, FaunaDB, and PlanetScale with pricing, scaling, query patterns, and migration strategies.