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