Problem 1 — Availability
HTTP 503 responses appeared during the burst. We need to determine whether the source is API Gateway, Lambda, or the call to OpenAI.
Technical Guide · Stadium Assistant
Walking Skeleton correction: identify the source of the 503s, fix the measurement, reduce latency, stabilize the OpenAI Responses API, and validate the p95 criterion again.
“Okay, now tell me step by step and in detail how to solve the problem of the responses that do not meet the required parameters.”
The objective is not only to get all 20 requests to finish. For the test to be considered passed, the report must correctly distinguish between successful and failed requests and evaluate latency only with valid data.
The exit criterion we must achieve is:
total = 20
successful = 20
failed = 0
successRatePct = 100
successfulP95Ms <= 1500
overallTargetMet = trueHTTP 503 responses appeared during the burst. We need to determine whether the source is API Gateway, Lambda, or the call to OpenAI.
Complete responses on the direct OpenAI route can exceed the 1500 ms target. Optimization must be measured separately from errors.
Do not change the model or performance code yet. First confirm the source of the error.
Open PowerShell in the DEV environment and first confirm that the Lambda function is not being limited by concurrency.
aws lambda get-function-concurrency `
--function-name sa-dev-orchestrator-chat `
--region us-east-1 `
--profile sa-devThen review the function configuration:
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}"Review the Lambda logs for the period when you ran the test:
aws logs tail "/aws/lambda/sa-dev-orchestrator-chat" `
--since 30m `
--region us-east-1 `
--profile sa-devAnd review the API Gateway logs:
aws logs tail "/aws/apigateway/sa-dev-http-api" `
--since 30m `
--region us-east-1 `
--profile sa-devThrottling messages, timeouts, integration 5xx errors, or an error coming from the OpenAI call. We must not assume the source without evidence.
scripts/test-api-burst.mjsThe script must separate successes and failures and calculate p50/p95 over successful responses.
Replace the contents of:
scripts/test-api-burst.mjswith a version that explicitly reports successful, failed, successRatePct, successfulP50Ms, successfulP95Ms and 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"For “Hola” you do not need additional reasoning. That work increases latency without adding value.
In the OpenAI Responses API call, configure:
reasoning: {
effort: "none"
}The idea is that a simple Walking Skeleton request should not consume reasoning time it does not need.
text.verbosity = "low"The Walking Skeleton response should be short. We do not want “Hola” to produce a long response.
text: {
verbosity: "low"
}For this test, the response should remain one or two sentences. The objective here is to validate the end-to-end pipeline, not generate long content.
max_output_tokensLimit generation so a simple response has no room to grow unnecessarily.
max_output_tokens: 80For the Walking Skeleton, 80 tokens are enough for a very short response and reduce generation time compared with much higher limits.
An automatic retry can turn a fast error into an extremely slow response.
Configure the OpenAI client explicitly:
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
maxRetries: 0,
timeout: 12000
});For this phase we want to observe the real error. If there is a 429, 5xx, or OpenAI timeout, it must remain visible in the logs and not be hidden behind multiple SDK retries.
We need to know whether a Lambda failure really comes from OpenAI and what type of failure it was.
Do not log the API key or sensitive content. Log only the operational information needed for diagnosis.
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, OpenAI 5xx error, application error, or integration error. Without this separation, we will not know what to optimize.
fast_path for the Walking Skeleton greetingThe original criterion tests “Hola”. You do not need to call OpenAI to resolve a completely deterministic greeting.
Before executing the direct OpenAI route, normalize the message and respond locally to simple greetings.
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?";
}In the 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
})
};
}After modifying the code, you must build deployment.zip again and upload it to the DEV function.
First run the existing local tests:
npm run test:health
npm run test:injection
npm run test:localGenerate the deployment package again using the project procedure. If your packaging script is already configured:
npm run packageThen update the Lambda function:
aws lambda update-function-code `
--function-name sa-dev-orchestrator-chat `
--zip-file fileb://deployment.zip `
--region us-east-1 `
--profile sa-devWait for the update to finish:
aws lambda wait function-updated `
--function-name sa-dev-orchestrator-chat `
--region us-east-1 `
--profile sa-devDo not jump directly to maximum load. Increase concurrency gradually to see where the problem appears.
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"Record the following in each test:
successful.failed.successRatePct.successfulP50Ms.successfulP95Ms.successfulMaxMs.overallTargetMet.20/20 successful, 0 failed, successRatePct 100, successful p95 ≤ 1500 ms, and overallTargetMet=true.
This step is performed only after you have evidence that AWS and the code are working correctly.
If the logs show 429, rate limit, or OpenAI saturation, review your project limits and concurrency pattern. Do not increase concurrency blindly.
If the direct model route remains stable but does not meet the latency target, treat that measurement as a metric separate from the greeting criterion. For the Walking Skeleton, the fast_path allows the pipeline to be validated without spending a full inference on a deterministic input.
Complete the checklist to confirm that the solution has been validated.
reasoning.effort = "none".text.verbosity = "low".max_output_tokens.