Skip to content
StackPractices
advanced Por Mathias Paulenko

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

RuntimeCold Start (promedio)Mejor Para
Go~100msAlta performance, low cold start
Node.js~200msWeb APIs, inicio rapido
Python~300msData processing, ML inference
.NET~500msEnterprise apps
Java~1000msFrameworks 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