Módulo 3: Serverless & Lambda for AI

5. Timeout y Memory Configuration para AI

Descripción

En esta cápsula vas a configurar timeout y memoria de Lambda específicamente para workloads de AI. No es "pon 128MB y 3 segundos" — una función que invoca GPT-4o necesita configuración radicalmente diferente a una que procesa un webhook. Al terminar, sabrás dimensionar Lambda para inferencia LLM, entender por qué más memoria significa más CPU, y usar Lambda Power Tuning para encontrar el punto óptimo entre velocidad y coste.

Contexto: Lambda te permite configurar dos recursos: timeout (máximo 15 minutos) y memoria (128MB a 10GB). Lo que muchos no saben es que la CPU se asigna proporcionalmente a la memoria — 1769MB te da 1 vCPU completa. Esto convierte la configuración de memoria en una decisión de CPU, que afecta directamente el rendimiento de tu función AI y cuánto pagas por cada invocación.


Timeout: El reloj que no perdona

Cómo funciona el timeout de Lambda

Lambda ejecuta tu función hasta que retorne un resultado o se agote el timeout. No hay extensiones, no hay negociación. Si tu función no termina a tiempo, Lambda la mata y retorna error.

Timeline de una invocación Lambda:
0s ──── cold start ──── init done ──── handler ejecuta ──── response
│                                                            │
└──────────── timeout window (configurado por ti) ───────────┘
              Si no termina aquí → KILLED → error 504

Configuración de timeout

# En tu template SAM / serverless.yml / Terraform

# SAM template
Resources:
  AiFunction:
    Type: AWS::Serverless::Function
    Properties:
      Timeout: 60  # segundos (máx: 900 = 15 min)
# Terraform
resource "aws_lambda_function" "ai_endpoint" {
  timeout = 60  # segundos
}
# AWS CLI
aws lambda update-function-configuration \
  --function-name ai-endpoint \
  --timeout 60

Por qué el timeout importa más para AI

Una Lambda que procesa un webhook tarda milisegundos. Una Lambda que invoca un LLM tarda segundos — a veces muchos:

Tiempos típicos de API LLM (p95):

Operación                          Tiempo típico
─────────────────────────────────────────────────
OpenAI gpt-4o-mini (100 tokens)   1-3s
OpenAI gpt-4o (500 tokens)        3-8s
OpenAI gpt-4o (2000 tokens)       8-20s
Anthropic Claude 3.5 (500 tokens) 2-6s
Prompt chain (3 llamadas)         10-30s
RAG: embed + search + generate    5-15s

El default de Lambda es 3 segundos. Con ese timeout, la mayoría de tus invocaciones a LLM fallan.

Estrategia de timeout para AI

Regla práctica:
timeout_lambda = timeout_llm_client × 1.5 + overhead

Donde:
- timeout_llm_client = timeout que configuras en tu SDK de OpenAI/Anthropic
- 1.5 = margen para retries internos del SDK
- overhead = 2-5s para cold start, parsing, logging
import os
from openai import OpenAI

LAMBDA_TIMEOUT = int(os.environ.get("LAMBDA_TIMEOUT", "60"))

# El client timeout debe ser MENOR que el Lambda timeout
# para que tu código maneje el error gracefully en vez de que Lambda lo mate
client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    timeout=LAMBDA_TIMEOUT - 10  # 10s de margen para cleanup
)

def handler(event, context):
    remaining_ms = context.get_remaining_time_in_millis()

    if remaining_ms < 15000:
        return {
            "statusCode": 408,
            "body": "Insufficient time remaining for LLM call"
        }

    try:
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": event["prompt"]}],
            max_tokens=500,
        )
        return {"statusCode": 200, "body": response.choices[0].message.content}
    except Exception as e:
        return {"statusCode": 504, "body": f"LLM timeout: {str(e)}"}

Tabla de timeouts recomendados

Caso de uso                  Client timeout  Lambda timeout
──────────────────────────────────────────────────────────
API call simple (1 LLM)     30s             45s
Prompt chain (2-3 calls)    30s por call    120s
RAG pipeline                45s             90s
Batch processing            60s por item    300s
Generación larga (>2K tok)  60s             90s

El truco de context.get_remaining_time_in_millis()

Lambda te da acceso al tiempo restante. Úsalo para tomar decisiones inteligentes:

def handler(event, context):
    remaining = context.get_remaining_time_in_millis()

    # Si queda poco tiempo, usa un modelo más rápido
    if remaining < 20000:
        model = "gpt-4o-mini"
        max_tokens = 200
    else:
        model = event.get("model", "gpt-4o")
        max_tokens = event.get("max_tokens", 1000)

    response = client.chat.completions.create(
        model=model,
        messages=event["messages"],
        max_tokens=max_tokens,
    )
    return {
        "statusCode": 200,
        "body": json.dumps({
            "content": response.choices[0].message.content,
            "model_used": model,
            "time_remaining_ms": context.get_remaining_time_in_millis()
        })
    }

Memory: No es solo RAM, es CPU

La relación memoria-CPU de Lambda

Lambda no te permite configurar CPU directamente. En su lugar, la CPU se escala proporcionalmente con la memoria:

Memoria    CPU (aprox)      Equivalencia
──────────────────────────────────────────
128 MB     ~0.07 vCPU       Inutilizable para AI
256 MB     ~0.14 vCPU       Muy lento
512 MB     ~0.29 vCPU       Mínimo viable para API calls
1024 MB    ~0.58 vCPU       Adecuado para 1 LLM call
1769 MB    1 vCPU           Sweet spot para AI APIs
3538 MB    2 vCPU           Procesamiento paralelo
5307 MB    3 vCPU           Embeddings locales
10240 MB   6 vCPU           Modelos en memoria (edge case)

Por qué esto cambia todo para AI

# Con 128MB (default): tu función tiene ~7% de un CPU
# Parsing JSON, serialización, manejo de respuesta = LENTO
# El SDK de OpenAI necesita CPU para TLS handshake, parsing

# Con 1769MB (1 vCPU): misma función, ~14x más CPU
# TLS handshake: 200ms → 15ms
# JSON parsing de response grande: 50ms → 4ms
# Total overhead: ~300ms → ~25ms

Configuración de memoria

# SAM template
Resources:
  AiFunction:
    Type: AWS::Serverless::Function
    Properties:
      MemorySize: 1769  # MB — 1 vCPU completa

# Terraform
resource "aws_lambda_function" "ai_endpoint" {
  memory_size = 1769
}

# AWS CLI
aws lambda update-function-configuration \
  --function-name ai-endpoint \
  --memory-size 1769

Right-sizing: LLM API calls vs procesamiento local

La memoria óptima depende de QUÉ hace tu función:

Escenario A: Lambda invoca OpenAI API
──────────────────────────────────────
El cuello de botella es NETWORK (esperar respuesta de OpenAI).
Tu función no necesita mucho CPU — solo enviar request y parsear response.

→ Recomendado: 512MB - 1024MB
→ Más memoria no hace que OpenAI responda más rápido

Escenario B: Lambda procesa embeddings localmente
──────────────────────────────────────────────────
El cuello de botella es CPU (cálculo de vectores, similarity search).

→ Recomendado: 3072MB - 5120MB
→ Más CPU = procesamiento más rápido = Lambda termina antes

Escenario C: Lambda hace RAG (embed + search + generate)
──────────────────────────────────────────────────────────
Combinación: algo de CPU para embeddings + espera de network para LLM.

→ Recomendado: 1769MB - 3072MB
→ Balance entre CPU para procesamiento y coste por segundo

El paradox: más memoria puede ser MÁS BARATO

Lambda cobra: precio × (memoria_GB) × (duración_segundos)

Ejemplo con una función que tarda:
- 128MB:  10 segundos → 10s × 0.125 GB = 1.25 GB-s
- 1024MB: 2 segundos  → 2s × 1.0 GB   = 2.0 GB-s   (más caro)
- 1769MB: 1.2 segundos → 1.2s × 1.73 GB = 2.08 GB-s (más caro)

PERO si la función está I/O bound (esperando a OpenAI):
- 128MB:  8 segundos → 8s × 0.125 GB = 1.0 GB-s
- 512MB:  7.5 segundos → 7.5s × 0.5 GB = 3.75 GB-s (más caro)
- 1769MB: 7.2 segundos → 7.2s × 1.73 GB = 12.46 GB-s (mucho más caro)

Conclusión: para funciones I/O bound (que es la mayoría de AI API calls),
más memoria NO ayuda y SÍ cuesta más. Mide antes de escalar.

Lambda Power Tuning

Qué es y por qué usarlo

Lambda Power Tuning es una herramienta open-source que ejecuta tu función con diferentes configuraciones de memoria y te muestra el punto óptimo entre coste y velocidad.

Resultado típico de Power Tuning:

Memoria   Duración   Coste         Relación
────────────────────────────────────────────
128 MB    12.3s      $0.0000253    Lento y barato
256 MB    8.1s       $0.0000333    Mejor
512 MB    4.2s       $0.0000344    Diminishing returns empieza
1024 MB   2.8s       $0.0000459    Más rápido pero más caro
1769 MB   2.5s       $0.0000710    CPU no ayuda (I/O bound)
3072 MB   2.4s       $0.0001178    Desperdicio

Cómo usarlo

# Paso 1: Desplegar la State Machine (una sola vez)
# Usa el SAR (Serverless Application Repository)
aws serverlessrepo create-cloud-formation-change-set \
  --application-id arn:aws:serverlessrepo:us-east-1:451282441545:applications/aws-lambda-power-tuning \
  --stack-name lambda-power-tuning

# Paso 2: Ejecutar el tuning
aws stepfunctions start-execution \
  --state-machine-arn arn:aws:states:us-east-1:ACCOUNT:stateMachine:powerTuningStateMachine \
  --input '{
    "lambdaARN": "arn:aws:lambda:us-east-1:ACCOUNT:function:ai-endpoint",
    "powerValues": [128, 256, 512, 1024, 1769, 3072],
    "num": 20,
    "payload": "{\"prompt\": \"Explain serverless in one sentence\"}"
  }'

Interpretar resultados para AI

Lo que típicamente verás en una función que hace API calls a LLM:

Velocidad vs Memoria para AI API call:

Duración (s)
│
12 ┤  ●
│
8  ┤     ●
│
4  ┤        ●
│
3  ┤           ●──●──●──●    ← Plateau: I/O bound
│
└──┬──┬──┬──┬──┬──┬──┬─ Memoria (MB)
  128 256 512 1K 1.7K 3K 5K

Después de ~512MB-1024MB, más memoria no reduce
la duración porque el cuello de botella es la red.

Conclusión práctica

Para funciones que llaman a APIs de LLM (el caso más común en esta guía):

Configuración recomendada:
├── Memory: 512MB - 1024MB
├── Timeout: 45-60s
├── Razón: I/O bound, más CPU no ayuda
└── Excepción: si haces JSON processing pesado, sube a 1769MB

Para funciones que procesan datos localmente (embeddings, transformaciones):

Configuración recomendada:
├── Memory: 1769MB - 3072MB
├── Timeout: 60-120s
├── Razón: CPU bound, más memoria = más CPU = más rápido
└── Excepción: si los datos caben en 512MB de RAM, no subas por CPU

Configuración Completa: Ejemplo AI Endpoint

# handler.py — Lambda handler con timeout y memory optimizados
import json
import os
import logging
import time
from openai import OpenAI

logger = logging.getLogger()
logger.setLevel(logging.INFO)

LAMBDA_TIMEOUT = int(os.environ.get("LAMBDA_TIMEOUT", "60"))
MIN_REMAINING_MS = 10000

client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    timeout=LAMBDA_TIMEOUT - 10,
    max_retries=1,
)

def handler(event, context):
    start_time = time.time()

    remaining_ms = context.get_remaining_time_in_millis()
    if remaining_ms < MIN_REMAINING_MS:
        logger.warning(f"Insufficient time: {remaining_ms}ms remaining")
        return {
            "statusCode": 408,
            "body": json.dumps({"error": "Insufficient time for LLM call"})
        }

    try:
        body = json.loads(event.get("body", "{}"))
        prompt = body.get("prompt", "")
        max_tokens = min(body.get("max_tokens", 500), 2000)
    except (json.JSONDecodeError, AttributeError):
        return {
            "statusCode": 400,
            "body": json.dumps({"error": "Invalid request body"})
        }

    if not prompt:
        return {
            "statusCode": 400,
            "body": json.dumps({"error": "prompt is required"})
        }

    try:
        response = client.chat.completions.create(
            model=os.environ.get("MODEL_NAME", "gpt-4o-mini"),
            messages=[
                {"role": "system", "content": "Responde de forma concisa y útil."},
                {"role": "user", "content": prompt}
            ],
            max_tokens=max_tokens,
        )

        duration_ms = round((time.time() - start_time) * 1000)
        answer = response.choices[0].message.content
        tokens = response.usage.total_tokens

        logger.info(f"LLM call completed in {duration_ms}ms, {tokens} tokens")

        return {
            "statusCode": 200,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps({
                "answer": answer,
                "tokens_used": tokens,
                "duration_ms": duration_ms,
                "remaining_ms": context.get_remaining_time_in_millis()
            })
        }

    except Exception as e:
        logger.error(f"LLM error after {round((time.time() - start_time) * 1000)}ms: {e}")
        return {
            "statusCode": 502,
            "body": json.dumps({"error": f"LLM call failed: {str(e)}"})
        }
# template.yaml (SAM) — Configuración optimizada para AI
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Runtime: python3.11
    Architectures:
      - arm64  # Graviton: 20% más barato, rendimiento similar para I/O bound

Resources:
  AiEndpoint:
    Type: AWS::Serverless::Function
    Properties:
      Handler: handler.handler
      CodeUri: ./src/
      MemorySize: 768  # Sweet spot para API calls: suficiente CPU sin desperdicio
      Timeout: 60
      Environment:
        Variables:
          OPENAI_API_KEY: !Ref OpenAiApiKey
          MODEL_NAME: gpt-4o-mini
          LAMBDA_TIMEOUT: "60"
      Events:
        ApiEvent:
          Type: Api
          Properties:
            Path: /ask
            Method: post

Parameters:
  OpenAiApiKey:
    Type: String
    NoEcho: true

Cold Start y su Relación con Memory

Más memoria también reduce el cold start — la inicialización tiene más CPU disponible:

Cold start típico para una función AI (con openai SDK):

Memoria     Cold start    Warm invocation
──────────────────────────────────────────
128 MB      ~8-12s        ~3-5s
512 MB      ~3-5s         ~2-4s
1024 MB     ~1.5-3s       ~2-3s
1769 MB     ~1-2s         ~2-3s
3072 MB     ~0.8-1.5s     ~2-3s

El cold start mejora con más memoria (más CPU para imports).
La invocación warm casi no cambia (I/O bound esperando a OpenAI).
# Optimizar imports para reducir cold start
# Los imports se ejecutan FUERA del handler (durante init)

# ✅ Importar solo lo necesario
from openai import OpenAI
import json
import os

# ❌ No importar librerías pesadas que no necesitas
# import pandas  # +500ms al cold start
# import numpy   # +300ms al cold start
# import langchain  # +800ms al cold start — importa solo los módulos específicos

Monitoreo de Timeout y Memory

CloudWatch Metrics

# Métricas automáticas que Lambda envía a CloudWatch:
# - Duration: tiempo de ejecución (ms)
# - Max Memory Used: memoria realmente usada (MB)
# - Timeouts: invocaciones que excedieron el timeout

# Consultar métricas con AWS CLI
aws cloudwatch get-metric-statistics \
  --namespace AWS/Lambda \
  --metric-name Duration \
  --dimensions Name=FunctionName,Value=ai-endpoint \
  --start-time 2026-03-01T00:00:00Z \
  --end-time 2026-03-08T00:00:00Z \
  --period 3600 \
  --statistics Average Maximum p99

Custom metrics para AI

import json
import time
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    timeout=50,
)

def handler(event, context):
    start = time.time()
    body = json.loads(event.get("body", "{}"))

    llm_start = time.time()
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": body["prompt"]}],
        max_tokens=500,
    )
    llm_duration = time.time() - llm_start

    total_duration = time.time() - start
    memory_used = context.memory_limit_in_mb

    # Structured logging para CloudWatch Insights
    print(json.dumps({
        "metric": "llm_call",
        "llm_duration_ms": round(llm_duration * 1000),
        "total_duration_ms": round(total_duration * 1000),
        "overhead_ms": round((total_duration - llm_duration) * 1000),
        "tokens": response.usage.total_tokens,
        "memory_configured_mb": memory_used,
        "remaining_ms": context.get_remaining_time_in_millis(),
    }))

    return {
        "statusCode": 200,
        "body": json.dumps({"answer": response.choices[0].message.content})
    }
# CloudWatch Insights query para analizar rendimiento
fields @timestamp, metric, llm_duration_ms, overhead_ms, tokens, memory_configured_mb
| filter metric = "llm_call"
| stats avg(llm_duration_ms) as avg_llm,
        max(llm_duration_ms) as p100_llm,
        avg(overhead_ms) as avg_overhead
  by bin(1h)

Arquitectura ARM64 (Graviton)

Lambda ofrece dos arquitecturas: x86_64 y arm64 (Graviton). Para funciones AI que llaman APIs:

x86_64 vs arm64 para AI API calls:

Aspecto           x86_64       arm64 (Graviton)
──────────────────────────────────────────────────
Precio            $0.0000166667/GB-s   $0.0000133334/GB-s
Diferencia        Base         20% más barato
Cold start        Similar      Similar (a veces 5% mejor)
Compatibilidad    Todo         Casi todo (excepto binarios x86)
SDK OpenAI        ✅            ✅
SDK Anthropic     ✅            ✅
numpy/scipy       ✅            ✅ (wheels arm64 disponibles)

Recomendación: usa arm64 siempre que no tengas dependencias
que requieran binarios x86 específicos.

Troubleshooting

Problema 1: "Task timed out after X seconds"

# La función excedió su timeout
# Diagnóstico:
aws logs filter-log-events \
  --log-group-name /aws/lambda/ai-endpoint \
  --filter-pattern "Task timed out"

# Soluciones:
# 1. Aumentar Lambda timeout
# 2. Reducir client timeout para manejar el error en tu código
# 3. Reducir max_tokens para respuestas más cortas
# 4. Usar un modelo más rápido (gpt-4o-mini vs gpt-4o)

Problema 2: "Runtime exited with error: signal: killed" (OOM)

# La función excedió su límite de memoria
# Lambda la mata inmediatamente (Out of Memory)

# Diagnóstico: busca "Max Memory Used" cerca del límite
aws cloudwatch get-metric-statistics \
  --namespace AWS/Lambda \
  --metric-name "Max Memory Used" \
  --dimensions Name=FunctionName,Value=ai-endpoint \
  --period 300 --statistics Maximum

# Si Max Memory Used > 90% de MemorySize → sube la memoria
# Para AI API calls, raramente necesitas más de 512MB de RAM real
# Si OOM ocurre, probablemente estás cargando algo grande en memoria

Problema 3: "La función es lenta pero no hace timeout"

# Posibles causas:
# 1. Memoria muy baja → CPU insuficiente para TLS handshake
#    Solución: sube a 512MB mínimo

# 2. Cold start — primera invocación después de inactividad
#    Solución: provisioned concurrency o keep-warm

# 3. El LLM está lento (no es tu Lambda)
#    Diagnóstico: revisa llm_duration_ms vs overhead_ms en logs
#    Si llm_duration_ms es >90% del total → el cuello es OpenAI, no Lambda

Problema 4: "Costes más altos de lo esperado"

# Diagnóstico: revisa duración real vs configurada
aws logs filter-log-events \
  --log-group-name /aws/lambda/ai-endpoint \
  --filter-pattern "REPORT" \
  --limit 20

# Cada línea REPORT muestra:
# Duration: 2345.67 ms  Billed Duration: 2400 ms  Memory Size: 1769 MB  Max Memory Used: 180 MB
#                                                   ^^^^^^^^^^^^^^^^^     ^^^^^^^^^^^^^^^^^^^^^^
#                                                   Memoria CONFIGURADA   Memoria REAL usada

# Si Max Memory Used << Memory Size → estás desperdiciando dinero
# Baja la memoria hasta que Max Memory Used sea ~70% de Memory Size

Ejercicios Prácticos

Ejercicio 1: Configura timeout adaptativo

Escribe un handler que use context.get_remaining_time_in_millis() para decidir: si queda más de 30s, usa gpt-4o con 1000 tokens; si queda entre 15-30s, usa gpt-4o-mini con 300 tokens; si queda menos de 15s, retorna error sin llamar al LLM.

Ver solución
import json
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"], timeout=50)

def handler(event, context):
    remaining_ms = context.get_remaining_time_in_millis()

    if remaining_ms < 15000:
        return {
            "statusCode": 408,
            "body": json.dumps({
                "error": "Not enough time for LLM call",
                "remaining_ms": remaining_ms
            })
        }

    if remaining_ms > 30000:
        model = "gpt-4o"
        max_tokens = 1000
    else:
        model = "gpt-4o-mini"
        max_tokens = 300

    body = json.loads(event.get("body", "{}"))
    response = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": body["prompt"]}],
        max_tokens=max_tokens,
    )

    return {
        "statusCode": 200,
        "body": json.dumps({
            "answer": response.choices[0].message.content,
            "model_used": model,
            "remaining_ms": context.get_remaining_time_in_millis()
        })
    }

Ejercicio 2: Calcula la memoria óptima

Tu función AI tiene estos datos de CloudWatch:

  • Con 256MB: duración promedio 8.2s, max memory used 120MB
  • Con 512MB: duración promedio 4.1s, max memory used 135MB
  • Con 1024MB: duración promedio 3.8s, max memory used 142MB
  • Con 1769MB: duración promedio 3.6s, max memory used 148MB

Calcula el coste por invocación para cada configuración y determina cuál es la óptima. Precio: $0.0000166667 por GB-segundo.

Ver solución
configs = [
    {"memory_mb": 256,  "duration_s": 8.2, "max_used_mb": 120},
    {"memory_mb": 512,  "duration_s": 4.1, "max_used_mb": 135},
    {"memory_mb": 1024, "duration_s": 3.8, "max_used_mb": 142},
    {"memory_mb": 1769, "duration_s": 3.6, "max_used_mb": 148},
]

price_per_gb_second = 0.0000166667

print(f"{'Memory':>8} {'Duration':>10} {'GB-s':>8} {'Cost/inv':>12} {'Used%':>8}")
print("─" * 52)

for c in configs:
    gb_seconds = (c["memory_mb"] / 1024) * c["duration_s"]
    cost = gb_seconds * price_per_gb_second
    used_pct = (c["max_used_mb"] / c["memory_mb"]) * 100
    print(f"{c['memory_mb']:>6}MB {c['duration_s']:>8.1f}s {gb_seconds:>8.2f} ${cost:>11.7f} {used_pct:>6.1f}%")

# Resultado:
#   Memory   Duration     GB-s     Cost/inv    Used%
# ────────────────────────────────────────────────────
#   256MB       8.2s     2.05  $0.0000342    46.9%
#   512MB       4.1s     2.05  $0.0000342    26.4%
#  1024MB       3.8s     3.80  $0.0000633    13.9%
#  1769MB       3.6s     6.23  $0.0001038     8.4%
#
# 256MB y 512MB cuestan lo mismo ($0.0000342), pero 512MB es 2x más rápido.
# La duración apenas mejora de 512MB a 1769MB (I/O bound).
# → Óptimo: 512MB — mismo coste que 256MB, el doble de rápido.
# → 1024MB+ solo si necesitas reducir 0.3-0.5s más y vale el 85% de aumento en coste.

Ejercicio 3: Structured logging para monitoring

Agrega logging estructurado a tu handler que emita: llm_duration_ms, total_duration_ms, overhead_ms, tokens_used, model, memory_configured_mb, y cold_start (booleano). El cold start se detecta con una variable global que se inicializa en True y se cambia a False después de la primera invocación.

Ver solución
import json
import os
import time
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"], timeout=50)

IS_COLD_START = True

def handler(event, context):
    global IS_COLD_START
    was_cold = IS_COLD_START
    IS_COLD_START = False

    start = time.time()
    body = json.loads(event.get("body", "{}"))

    llm_start = time.time()
    response = client.chat.completions.create(
        model=os.environ.get("MODEL_NAME", "gpt-4o-mini"),
        messages=[{"role": "user", "content": body["prompt"]}],
        max_tokens=body.get("max_tokens", 500),
    )
    llm_end = time.time()

    total_end = time.time()

    llm_ms = round((llm_end - llm_start) * 1000)
    total_ms = round((total_end - start) * 1000)

    print(json.dumps({
        "event": "llm_invocation",
        "cold_start": was_cold,
        "llm_duration_ms": llm_ms,
        "total_duration_ms": total_ms,
        "overhead_ms": total_ms - llm_ms,
        "tokens_used": response.usage.total_tokens,
        "model": response.model,
        "memory_configured_mb": int(context.memory_limit_in_mb),
    }))

    return {
        "statusCode": 200,
        "body": json.dumps({
            "answer": response.choices[0].message.content,
            "cold_start": was_cold,
        })
    }

Ejercicio 4: Timeout safety net con fallback

Implementa un handler que, si la llamada al LLM principal (gpt-4o) tarda más de 20 segundos, la cancele y reintente con gpt-4o-mini con un prompt simplificado. Usa asyncio con wait_for o threading con timeout.

Ver solución
import json
import os
import time
from concurrent.futures import ThreadPoolExecutor, TimeoutError
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"], timeout=25)
executor = ThreadPoolExecutor(max_workers=1)

def call_llm(model, prompt, max_tokens):
    return client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
        max_tokens=max_tokens,
    )

def handler(event, context):
    body = json.loads(event.get("body", "{}"))
    prompt = body["prompt"]

    model_used = "gpt-4o"
    fallback_used = False

    try:
        future = executor.submit(call_llm, "gpt-4o", prompt, 1000)
        response = future.result(timeout=20)
    except TimeoutError:
        model_used = "gpt-4o-mini"
        fallback_used = True
        simplified = f"Responde brevemente: {prompt}"
        response = call_llm("gpt-4o-mini", simplified, 300)
    except Exception as e:
        return {
            "statusCode": 502,
            "body": json.dumps({"error": str(e)})
        }

    return {
        "statusCode": 200,
        "body": json.dumps({
            "answer": response.choices[0].message.content,
            "model_used": model_used,
            "fallback_used": fallback_used,
            "tokens": response.usage.total_tokens,
        })
    }

Resumen

  • Timeout default de Lambda (3s) es insuficiente para AI. Configura 45-60s mínimo para LLM API calls.
  • El timeout del cliente OpenAI/Anthropic debe ser MENOR que el timeout de Lambda — para que tu código maneje el error, no Lambda.
  • context.get_remaining_time_in_millis() te permite tomar decisiones inteligentes (modelo rápido vs lento, abortar si no hay tiempo).
  • Más memoria = más CPU (proporcional). 1769MB = 1 vCPU completa.
  • Para AI API calls (I/O bound), 512-1024MB es el sweet spot. Más memoria no hace que OpenAI responda más rápido.
  • Para procesamiento local (CPU bound), 1769-3072MB. Más CPU = más rápido = menor duración.
  • Lambda Power Tuning te da datos reales para tu función — no adivines, mide.
  • arm64 (Graviton) es 20% más barato con rendimiento similar para AI workloads.
  • Monitorea Memory Used vs Memory Configured. Si usas 150MB de 1769MB configurados, estás tirando dinero.

Recursos Adicionales

  1. Lambda Memory and CPU — Documentación oficial de configuración de memoria
  2. Lambda Power Tuning — Herramienta open-source para optimizar memoria
  3. Lambda Timeout Best Practices — Configuración de timeout
  4. Lambda Graviton2 (arm64) — Arquitectura ARM para Lambda
  5. CloudWatch Insights for Lambda — Queries para analizar performance
  6. OpenAI API Latency — Optimización de latencia en llamadas a OpenAI
  7. Lambda Pricing — Modelo de precios actual
  8. AWS Well-Architected Serverless Lens — Best practices serverless