Problema 1 — Disponibilidad
Durante la ráfaga aparecieron respuestas HTTP 503. Es necesario determinar si el origen es API Gateway, Lambda o la llamada a OpenAI.
Guía técnica · Stadium Assistant
Corrección del Walking Skeleton: identificar el origen de los 503, corregir la medición, reducir latencia, estabilizar OpenAI Responses API y volver a validar el criterio p95.
“Ok, ahora indicame el paso a paso y detalle a detalle para la solucion del problema de la respuesta que no cumplen con los parametros requeridos.”
El objetivo no es solamente conseguir que las 20 solicitudes terminen. Para considerar la prueba como aprobada, el reporte debe distinguir correctamente entre solicitudes exitosas y fallidas y evaluar la latencia únicamente con datos válidos.
El criterio de salida que debemos conseguir es:
total = 20
successful = 20
failed = 0
successRatePct = 100
successfulP95Ms <= 1500
overallTargetMet = trueDurante la ráfaga aparecieron respuestas HTTP 503. Es necesario determinar si el origen es API Gateway, Lambda o la llamada a OpenAI.
Las respuestas completas de la ruta directa OpenAI pueden superar la meta de 1500 ms. La optimización debe medirse separadamente de los errores.
No cambies todavía el modelo ni el código de performance. Primero confirma el origen del error.
Abre PowerShell en el ambiente DEV y confirma primero que la Lambda no esté siendo limitada por concurrencia.
aws lambda get-function-concurrency `
--function-name sa-dev-orchestrator-chat `
--region us-east-1 `
--profile sa-devDespués revisa la configuración de la función:
aws lambda get-function-configuration `
--function-name sa-dev-orchestrator-chat `
--region us-east-1 `
--profile sa-dev `
--query "{State:State,LastUpdateStatus:LastUpdateStatus,Timeout:Timeout,MemorySize:MemorySize,Runtime:Runtime}"Revisa los logs de Lambda del periodo donde ejecutaste la prueba:
aws logs tail "/aws/lambda/sa-dev-orchestrator-chat" `
--since 30m `
--region us-east-1 `
--profile sa-devY revisa los logs de API Gateway:
aws logs tail "/aws/apigateway/sa-dev-http-api" `
--since 30m `
--region us-east-1 `
--profile sa-devMensajes de throttling, timeout, 5xx de integración o un error proveniente de la llamada a OpenAI. No debemos asumir el origen sin evidencia.
scripts/test-api-burst.mjsEl script debe separar éxitos y fallos y calcular p50/p95 sobre las respuestas exitosas.
Reemplaza el contenido de:
scripts/test-api-burst.mjspor una versión que reporte de forma explícita successful, failed, successRatePct, successfulP50Ms, successfulP95Ms y overallTargetMet.
const args = process.argv.slice(2);
function getArg(name, fallback) {
const index = args.indexOf(`--${name}`);
return index >= 0 && args[index + 1] !== undefined
? args[index + 1]
: fallback;
}
const url = getArg(
"url",
process.env.CHAT_URL ||
"https://w7jtrco599.execute-api.us-east-1.amazonaws.com/dev/chat"
);
const total = Number(getArg("total", "20"));
const concurrency = Number(getArg("concurrency", "5"));
const delayMs = Number(getArg("delay-ms", "200"));
const message = getArg("message", "Hola");
const targetP95Ms = Number(getArg("target-p95-ms", "1500"));
function percentile(values, p) {
if (!values.length) return null;
const sorted = [...values].sort((a, b) => a - b);
const index = Math.ceil((p / 100) * sorted.length) - 1;
return sorted[Math.max(0, index)];
}
const results = [];
let nextIndex = 0;
async function sendOne(index) {
if (delayMs > 0 && index > 0) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
const startedAt = performance.now();
let status = 0;
let body = "";
let error = null;
try {
const response = await fetch(url, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
message,
session_id: `burst-${Date.now()}-${index}`,
locale: "es"
})
});
status = response.status;
body = await response.text();
} catch (err) {
error = err instanceof Error ? err.message : String(err);
}
const latencyMs = Math.round(performance.now() - startedAt);
const ok = status >= 200 && status < 300 && !error;
results.push({
index: index + 1,
ok,
status,
latencyMs,
error,
body: body.slice(0, 300)
});
console.log(
`[${index + 1}/${total}] status=${status || "ERR"} ` +
`latency_ms=${latencyMs} ok=${ok}`
);
}
async function worker() {
while (true) {
const index = nextIndex++;
if (index >= total) return;
await sendOne(index);
}
}
await Promise.all(
Array.from({ length: Math.min(concurrency, total) }, () => worker())
);
const successfulResults = results.filter((item) => item.ok);
const failedResults = results.filter((item) => !item.ok);
const successfulLatencies = successfulResults.map((item) => item.latencyMs);
const successful = successfulResults.length;
const failed = failedResults.length;
const successRatePct = Number(((successful / total) * 100).toFixed(2));
const successfulP50Ms = percentile(successfulLatencies, 50);
const successfulP95Ms = percentile(successfulLatencies, 95);
const successfulMaxMs = successfulLatencies.length
? Math.max(...successfulLatencies)
: null;
const overallTargetMet =
total === 20 &&
successful === 20 &&
failed === 0 &&
successRatePct === 100 &&
successfulP95Ms !== null &&
successfulP95Ms <= targetP95Ms;
const statusCounts = results.reduce((acc, item) => {
const key = String(item.status || "ERR");
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
console.log("\n=== RESULT ===");
console.log(JSON.stringify({
url,
message,
total,
concurrency,
delayMs,
successful,
failed,
successRatePct,
successfulP50Ms,
successfulP95Ms,
successfulMaxMs,
targetP95Ms,
overallTargetMet,
statusCounts,
failures: failedResults
}, null, 2));
process.exit(overallTargetMet ? 0 : 1);reasoning.effort = "none"Para “Hola” no necesitas razonamiento adicional. Ese trabajo aumenta latencia sin aportar valor.
En la llamada a OpenAI Responses API configura:
reasoning: {
effort: "none"
}La idea es que una solicitud simple del Walking Skeleton no consuma tiempo de razonamiento que no necesita.
text.verbosity = "low"La respuesta del Walking Skeleton debe ser corta. No queremos que “Hola” produzca una respuesta larga.
text: {
verbosity: "low"
}Para esta prueba la respuesta debe mantenerse en una o dos frases. El objetivo aquí es validar el tubo end-to-end, no generar contenido largo.
max_output_tokensLimita la generación para que una respuesta simple no tenga margen para crecer innecesariamente.
max_output_tokens: 80Para el Walking Skeleton, 80 tokens son suficientes para una respuesta muy corta y reducen el tiempo de generación frente a límites mucho mayores.
Un retry automático puede convertir un error rápido en una respuesta extremadamente lenta.
Configura el cliente de OpenAI de forma explícita:
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
maxRetries: 0,
timeout: 12000
});Para esta fase queremos observar el error real. Si existe un 429, 5xx o timeout de OpenAI, debe quedar visible en logs y no quedar escondido detrás de múltiples retries del SDK.
Necesitamos saber si una falla de la Lambda realmente proviene de OpenAI y qué tipo de falla fue.
No registres la API key ni el contenido sensible. Registra únicamente información operativa necesaria para diagnóstico.
try {
const startedOpenAI = Date.now();
const response = await openai.responses.create({
model: process.env.OPENAI_MODEL,
reasoning: { effort: "none" },
text: { verbosity: "low" },
max_output_tokens: 80,
instructions:
"Responde en el idioma del usuario. " +
"Para un saludo, responde brevemente en una o dos frases.",
input: message
});
console.log(JSON.stringify({
event: "openai_call_ok",
trace_id,
model: process.env.OPENAI_MODEL,
openai_latency_ms: Date.now() - startedOpenAI,
response_id: response.id,
route: "direct"
}));
return response.output_text;
} catch (error) {
console.error(JSON.stringify({
event: "openai_call_error",
trace_id,
name: error?.name,
status: error?.status,
code: error?.code,
type: error?.type,
message: error?.message,
request_id: error?.request_id,
route: "direct"
}));
throw error;
}Timeout, rate limit, error 5xx de OpenAI, error de aplicación o error de integración. Sin esta separación no sabremos qué optimizar.
fast_path para el saludo del Walking SkeletonEl criterio original prueba “Hola”. No necesitas llamar a OpenAI para resolver un saludo completamente determinista.
Antes de ejecutar la ruta directa a OpenAI, normaliza el mensaje y responde localmente a saludos simples.
function normalizeMessage(value = "") {
return value
.trim()
.toLocaleLowerCase("es")
.replace(/[¡!¿?.,]/g, "");
}
function getFastPathResponse(message, locale = "es") {
const normalized = normalizeMessage(message);
const greetings = new Set([
"hola",
"hello",
"hi",
"buenas",
"buen dia",
"buenos dias",
"buenas tardes",
"buenas noches"
]);
if (!greetings.has(normalized)) {
return null;
}
if (locale === "en") {
return "Hello! I’m the Stadium Assistant. How can I help you?";
}
return "¡Hola! Soy el Stadium Assistant. ¿En qué puedo ayudarte?";
}En el handler:
const fastPathResponse = getFastPathResponse(message, locale);
if (fastPathResponse) {
const latencyMs = Date.now() - startedAt;
console.log(JSON.stringify({
trace_id,
latency_ms: latencyMs,
status: 200,
route: "fast_path"
}));
return {
statusCode: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({
userMessage: message,
assistantResponse: fastPathResponse,
trace_id
})
};
}Después de modificar el código, debes construir otra vez deployment.zip y subirlo a la función DEV.
Primero ejecuta las pruebas locales existentes:
npm run test:health
npm run test:injection
npm run test:localGenera de nuevo el paquete de despliegue con el procedimiento del proyecto. Si tu script de empaquetado ya está configurado:
npm run packageDespués actualiza la Lambda:
aws lambda update-function-code `
--function-name sa-dev-orchestrator-chat `
--zip-file fileb://deployment.zip `
--region us-east-1 `
--profile sa-devEspera a que termine la actualización:
aws lambda wait function-updated `
--function-name sa-dev-orchestrator-chat `
--region us-east-1 `
--profile sa-devNo saltes directamente a la carga máxima. Sube la concurrencia gradualmente para ver dónde aparece el problema.
node .\scripts\test-api-burst.mjs `
--url "https://w7jtrco599.execute-api.us-east-1.amazonaws.com/dev/chat" `
--total 20 `
--concurrency 1 `
--delay-ms 200 `
--message "Hola"node .\scripts\test-api-burst.mjs `
--url "https://w7jtrco599.execute-api.us-east-1.amazonaws.com/dev/chat" `
--total 20 `
--concurrency 5 `
--delay-ms 200 `
--message "Hola"node .\scripts\test-api-burst.mjs `
--url "https://w7jtrco599.execute-api.us-east-1.amazonaws.com/dev/chat" `
--total 20 `
--concurrency 10 `
--delay-ms 200 `
--message "Hola"node .\scripts\test-api-burst.mjs `
--url "https://w7jtrco599.execute-api.us-east-1.amazonaws.com/dev/chat" `
--total 20 `
--concurrency 20 `
--delay-ms 200 `
--message "Hola"En cada prueba registra:
successful.failed.successRatePct.successfulP50Ms.successfulP95Ms.successfulMaxMs.overallTargetMet.20/20 exitosas, 0 fallidas, successRatePct 100, p95 exitoso ≤ 1500 ms y overallTargetMet=true.
Este paso se hace solamente después de tener evidencia de que AWS y el código están funcionando correctamente.
Si los logs muestran 429, rate limit o saturación de OpenAI, revisa los límites de tu proyecto y el patrón de concurrencia. No aumentes concurrencia a ciegas.
Si la ruta directa al modelo sigue estable pero no alcanza la latencia objetivo, trata esa medición como una métrica separada del criterio del saludo. Para el Walking Skeleton, el fast_path permite validar el tubo sin gastar una inferencia completa para una entrada determinista.
Completa el checklist para confirmar que la solución quedó validada.
reasoning.effort = "none".text.verbosity = "low".max_output_tokens.