1. ¿Para qué necesita ChatGPT App pruebas de carga?
En la web clásica, las pruebas de carga suelen asociarse con la imagen de «millones de RPS, clúster gigante, pizza para SRE». Para ChatGPT App y los servidores MCP la realidad es más sencilla y, por suerte, más barata. En principio ya conoces los SLO, pero veamos cómo SLO/observability y la calidad del feed interactúan bajo carga.
La característica principal: ChatGPT espera a que termine el tool call para continuar generando la respuesta. El usuario ve un bonito stream de tokens, pero en cuanto el modelo decide invocar una herramienta, la magia del streaming se acaba — hasta que el backend responde. Si tu servidor MCP o ACP a veces responde en 8–10 segundos en vez de los 2–4 objetivo, la UX pasa de «asistente mágico» a «otro sitio lento más».
Además, hay un presupuesto de timeout estricto: para las llamadas a herramientas, OpenAI mantiene un límite superior del orden de decenas de segundos (las cifras exactas dependen del modo, pero hay que pensar en 30–60 segundos, y desde el punto de vista de UX — incluso hasta 5–10 segundos). Si en un pico de carga tus tool calls de repente pasan a tardar 25–30 segundos, formalmente sigues en el límite, pero para el usuario ya «estás roto».
Segundo punto: nos importa menos el RPS abstracto y más la concurrencia. Para una App del Store es bastante realista tener 50–100 usuarios activos simultáneos; eso es exactamente lo que quieres verificar, no «si aguanta 50k RPS de un GET /health sintético».
Y por último, ChatGPT App es una pila:
flowchart LR Usuario --> ChatGPT ChatGPT -->|tools/call| MCP["Servidor MCP GiftGenius"] MCP --> DB["Base con feed de regalos"] MCP --> ACP["Checkout / backend ACP"] ACP --> PSP["Pasarela de pago / Stripe"]
Si no comprobamos cómo vive esta pila bajo una carga pequeña pero realista, cualquier campaña de promoción o una aparición en una selección del Store puede convertirla rápidamente en una diapositiva de «cómo no se deben hacer productos LLM».
En esta lección, por «pruebas de carga ligeras» entenderemos ejecuciones cortas (normalmente 1–10 minutos) que comprueban:
- si el sistema soporta el pico de usuarios esperados;
- si la latencia p95/p99 no se dispara por encima de los SLO;
- si no aparecen errores, timeouts y rate limits de APIs externas.
Y en paralelo veremos la otra cara de la calidad: los datos del feed de productos (product feed, en adelante simplemente «feed»), sin los cuales ningún GiftGenius será ni «Gift» ni «Genius».
Primero resolveremos las pruebas de carga ligeras para MCP/ACP (qué y cómo cargar, qué métricas mirar), luego lo aterrizaremos en observabilidad (latencia, errores, recursos, webhooks y logs), y en la segunda mitad hablaremos de la calidad del feed y de cómo, bajo carga, puede sorprender.
2. Qué cargar exactamente: no ChatGPT, sino tus propios API
Es importante fijar una idea para no confundirla después: las pruebas de carga las hacemos directamente contra nuestro backend — servidor MCP, endpoints de ACP, webhooks — y no a través del UI de ChatGPT.
Hay varias razones.
- Primero, ahorro. Si ejecutas tool calls reales a través de ChatGPT, pagarás por tokens y además te toparás con los límites de ChatGPT, aunque estás probando tu propio código.
- Segundo, predictibilidad. Con llamadas directas a /mcp o /api/checkout controlas el escenario, sin depender de si el modelo decide invocar esa herramienta o no.
- Tercero, transparencia. Bajo carga quieres ver con claridad: aquí 2000 solicitudes a MCP en 5 minutos, aquí la distribución de latencia, aquí el gráfico de CPU. Si ejecutas la carga a través de ChatGPT, la capa adicional de ruido y restricciones solo complica el panorama.
Conjunto típico de endpoints para una prueba de carga de GiftGenius:
- endpoint del servidor MCP que implementa herramientas JSON‑RPC (/mcp o similar);
- uno o dos endpoints de ACP para crear y finalizar el checkout (en modo sandbox de la pasarela de pago);
- posiblemente — un endpoint que procesa los webhooks de la pasarela para ver cómo se comporta ante un pico de eventos.
Supondremos que tenemos un backend Next.js 16 donde vive el servidor MCP accesible en /api/mcp, y un servidor ACP con el endpoint /api/checkout/create.
3. Mini‑escenario de smoke‑load para GiftGenius
Imaginemos que nuestros product managers creen en un futuro brillante y dicen: «Un pico realista — 50 usuarios simultáneos; cada uno entra, elige un regalo y a veces llega al pago».
Para una prueba de carga ligera basta con modelar, digamos, 30–50 «usuarios virtuales» (VU), cada uno de los cuales realiza la secuencia:
- Llamada a la herramienta giftgenius.search_gifts (búsqueda de regalos por perfil y presupuesto).
- Llamada a giftgenius.get_gift_details para un par de productos del resultado.
- (A veces) llamada al endpoint de ACP create_checkout_session para un producto.
Todo esto directamente por HTTP a nuestro MCP/ACP, sin ChatGPT.
Llamada JSON‑RPC a MCP
Ejemplo de cuerpo de la solicitud a MCP (simplificado):
const body = {
jsonrpc: "2.0",
id: "test-" + Math.random(),
method: "tools/call",
params: {
toolName: "giftgenius.search_gifts",
arguments: {
occasion: "birthday",
budget: 50,
interests: ["sport", "books"],
},
},
};
En un proyecto real la estructura puede variar un poco, pero el principio es el mismo: un método JSON‑RPC y, dentro, la tool y sus argumentos.
4. Escribimos un script de carga simple en TypeScript
Como primer paso, implementemos la parte más simple de nuestro escenario: la llamada a giftgenius.search_gifts a MCP. Primero, haremos un script minimalista de Node.js en TypeScript que envía dichas solicitudes a /api/mcp y mide la latencia; luego añadiremos checkout y recorridos más complejos.
Cliente HTTP básico
Supongamos que tenemos un .env con MCP_URL=http://localhost:3000/api/mcp.
// scripts/loadTest.ts
import "dotenv/config";
const MCP_URL = process.env.MCP_URL!;
async function callSearchGifts() {
const body = {
jsonrpc: "2.0",
id: `search-${Date.now()}-${Math.random()}`,
method: "tools/call",
params: {
toolName: "giftgenius.search_gifts",
arguments: { occasion: "birthday", budget: 50 },
},
};
const started = Date.now();
const res = await fetch(MCP_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const latencyMs = Date.now() - started;
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return latencyMs;
}
Aquí también puedes añadir un parseo sencillo de la respuesta JSON, pero para fines de latencia y tasa de errores, esto es suficiente.
Ejecución concurrente de varias solicitudes
Necesitamos controlar la cantidad de solicitudes simultáneas. Para simplificar, tomemos un número fijo de «usuarios virtuales» y pidamos a cada uno que haga N solicitudes seguidas.
async function runVirtualUser(iterations: number) {
const latencies: number[] = [];
for (let i = 0; i < iterations; i++) {
try {
const ms = await callSearchGifts();
latencies.push(ms);
} catch (e) {
console.error("Error in VU:", e);
latencies.push(-1); // marcamos el error
}
}
return latencies;
}
Ahora podemos lanzar, por ejemplo, 20 de estos usuarios virtuales:
async function main() {
const users = 20;
const iterations = 10;
const tasks = Array.from({ length: users }, () =>
runVirtualUser(iterations),
);
const results = await Promise.all(tasks);
const all = results.flat();
// ...cálculo de métricas
}
main().catch((e) => console.error(e));
Esto ya proporcionará unas 200 llamadas a MCP, algunas de las cuales se ejecutarán en paralelo, es decir, con una concurrencia suficientemente alta.
Cálculo de p95 y tasa de errores
Añadamos una pequeña utilidad para calcular percentiles y errores. Recordemos: p95 es el valor por debajo del cual cae el 95% de las solicitudes.
function percentile(values: number[], p: number) {
const sorted = values.filter(v => v >= 0).sort((a, b) => a - b);
if (!sorted.length) return 0;
const idx = Math.floor((p / 100) * (sorted.length - 1));
return sorted[idx];
}
function errorRate(values: number[]) {
const total = values.length;
const errors = values.filter(v => v < 0).length;
return (errors / total) * 100;
}
Y en main añadimos la salida:
const p95 = percentile(all, 95);
const p99 = percentile(all, 99);
const errRate = errorRate(all);
console.log(`Total: ${all.length}`);
console.log(`p95: ${p95} ms, p99: ${p99} ms`);
console.log(`Error rate: ${errRate.toFixed(2)}%`);
Ya tienes un script de smoke‑load mínimo que puedes ejecutar localmente o en staging antes de un release. No tocas ChatGPT, no quemas tokens, y toda la atención se centra en tu MCP.
Qué hacer con ACP y checkout
De forma análoga puedes añadir otro helper callCreateCheckoutSession, que golpee al endpoint de ACP. Es importante usar el modo de pruebas/sandbox de la pasarela para no inflar pedidos reales. Una llamada típica será un POST con JSON:
async function callCreateCheckoutSession(productId: string) {
const started = Date.now();
const res = await fetch("http://localhost:3000/api/checkout/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ productId, test: true }),
});
const latencyMs = Date.now() - started;
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return latencyMs;
}
Luego, en runVirtualUser puedes aplicar el patrón: 3 búsquedas → 1 checkout, para simular el embudo «más búsquedas que compras».
5. Herramientas más serias: k6 (pero de forma sencilla)
El script en Node es bueno como «entrada mínima», pero a veces conviene usar una herramienta especializada como k6, donde los escenarios se escriben en JavaScript y el runtime es Go (es decir, rápido).
Ejemplo de un pequeño script de k6 para MCP:
// loadtest-mcp.js
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
stages: [
{ duration: "30s", target: 30 },
{ duration: "2m", target: 30 },
],
};
export default function () {
const payload = JSON.stringify({
jsonrpc: "2.0",
id: `search-${Math.random()}`,
method: "tools/call",
params: {
toolName: "giftgenius.search_gifts",
arguments: { occasion: "birthday", budget: 50 },
},
});
const res = http.post(__ENV.MCP_URL, payload, {
headers: { "Content-Type": "application/json" },
});
check(res, { "status is 200": (r) => r.status === 200 });
sleep(1);
}
Comando de ejecución:
MCP_URL=http://localhost:3000/api/mcp k6 run loadtest-mcp.js
k6 calculará por sí mismo p95/p99 y la tasa de errores, dibujará informes bonitos — y luego podrás exportarlos a Grafana y otros sistemas.
Lo importante es que, incluso con estas herramientas, nuestro objetivo sigue siendo el mismo: no aguantar un millón de RPS, sino asegurarnos de que con 5–10× sobre el pico esperado el sistema no se desmorona y el p95 se mantiene dentro de los SLO.
6. Qué observar durante (y después de) la ejecución de carga
Ya hablamos de métricas y SLO; ahora simplemente «aterricémoslo» en el contexto de carga.
Primero, la latencia. Para herramientas MCP como search_gifts te marcaste de antemano un objetivo del tipo «p95 < 2–3 segundos». Durante el smoke‑load mira si los p95/p99 no se han disparado 2–3 veces. Es importante comparar con el baseline: si antes de un cambio de código el p95 era 400 ms y después — 1500 ms, aunque formalmente sigas en SLO, ya es motivo para pensar.
Segundo, la tasa de errores. Bajo carga suelen aparecer cosas inesperadas: pool de conexiones a BD agotado, 429 inesperados de un API externo, timeouts al llamar a la pasarela de pago. Con carga normal la tasa de errores debe ser cercana a cero; en smoke‑load se toleran fallos puntuales, pero desde luego no 5–10%.
Tercero, métricas de recursos: CPU, memoria, a veces — número de descriptores de archivos abiertos y conexiones. Dependen de tu infraestructura, pero la idea clave es simple: no quieres ver que con 30 VU el CPU está al 100% y el GC se come la mitad del tiempo.
Cuarto, los webhooks. Si tienes un escenario de comercio, el punto final del pedido a menudo depende del procesamiento exitoso del webhook de la pasarela. Es importante mirar no solo la rapidez de la solicitud en ACP, sino también la latencia «llegó el webhook → lo procesamos correctamente».
Y por último, los logs. Logs estructurados con trace_id/checkout_session_id permiten, después de la ejecución de carga, tomar un par de solicitudes más lentas o fallidas y recorrer la cadena: MCP → API externo → ACP → webhook. Esto es especialmente útil si bajo carga ves colas extrañas en el p99.
7. Calidad de los datos del feed: de la estructura al sentido
Ya vimos cómo se comportan bajo carga la latencia, los errores y los recursos. Pero aunque cumplas los SLO en todos ellos, la experiencia de usuario puede «desmoronarse» por datos de mala calidad.
Pasamos al segundo gran tema: los datos. En una App de comercio como GiftGenius, el product feed (feed de productos) no es «algo en disco», sino literalmente el combustible para la LLM y los agentes. Si el feed es basura, el modelo no «inventará» por ti el precio y la disponibilidad.
Es útil pensar la calidad del feed en tres capas.
Nivel estructural
Se trata de la validez básica de los datos:
- El JSON se parsea correctamente.
- Todos los campos obligatorios están presentes: id, name, price, currency, imageUrl, availability, etc.
- Los tipos de los valores se ajustan a lo esperado: el precio — número, availability — enum, categories — array de strings.
- No hay duplicados de id.
Parte de esto ya lo cubriste con pruebas de contrato, cuando describiste el JSON Schema/esquema Zod para el feed. Ahora hay que aplicar esos esquemas a volúmenes reales de datos.
Ejemplo de un esquema Zod simple para un elemento del feed de GiftGenius:
import { z } from "zod";
export const giftItemSchema = z.object({
id: z.string().min(1),
name: z.string().min(3),
description: z.string().optional(),
price: z.number().positive(),
currency: z.enum(["USD", "EUR", "GBP"]),
imageUrl: z.string().url(),
inStock: z.boolean(),
tags: z.array(z.string()).default([]),
});
Y el esquema de todo el feed — simplemente z.array(giftItemSchema).
Nivel de negocio (semántica)
Estructuralmente un producto puede ser válido, pero desde el punto de vista de negocio — absurdo:
- Precio 0 o 0.01 para un producto caro.
- La moneda no corresponde al mercado (USD para productos que solo se venden en EUR).
- inStock = true, pero la fecha de última actualización fue hace medio año.
- Categorías entre 1000 variantes sin unificación.
Para este nivel conviene añadir comprobaciones adicionales y «reglas de sentido común». Por ejemplo:
const businessRules = (item: GiftItem) => {
const problems: string[] = [];
if (item.price > 10000) {
problems.push("precio sospechosamente alto");
}
if (!item.inStock && item.tags.includes("bestseller")) {
problems.push("bestseller, pero no disponible");
}
return problems;
};
Estas comprobaciones pueden ejecutarse como parte de una tarea nocturna o al generar un nuevo feed.
Nivel LLM
El modelo es muy listo, pero tiene sus «manías»:
- Descripciones repletas de HTML, etiquetas innecesarias y texto técnico.
- Idiomas mezclados (medio feed en ruso y medio en inglés) sin indicar el locale.
- «Nombres SEO» muy largos del estilo «Comprar el mejor súper súper regalo urgente barato».
En este nivel es importante llevar los datos a un formato amigable:
- Quitar las etiquetas HTML o convertirlas a texto plano.
- Normalizar el idioma de las descripciones (o al menos indicar explícitamente el locale).
- Recortar nombres excesivamente largos y la información duplicada.
Estas tareas pueden automatizarse en parte (por ejemplo, con scripts de pre‑procesamiento) y, en parte, acordarse con el equipo que alimenta el feed.
8. Práctica: validador del feed para GiftGenius
Añadamos a nuestro proyecto un script sencillo validateFeed.ts que lea un JSON con el feed, lo valide con Zod y calcule métricas básicas de calidad.
// scripts/validateFeed.ts
import { readFile } from "fs/promises";
import { giftItemSchema } from "../src/schema/giftItem";
async function main() {
const raw = await readFile("data/gift-feed.json", "utf-8");
const data = JSON.parse(raw);
const items = giftItemSchema.array().parse(data);
console.log(`Total de productos: ${items.length}`);
const missingImages = items.filter(i => !i.imageUrl).length;
console.log(`Sin imágenes: ${missingImages}`);
}
main().catch((e) => {
console.error("Feed validation failed:", e);
process.exit(1);
});
Aquí usamos el mismo contrato que en el servidor MCP, es decir, las pruebas de contrato y la verificación del feed usan el mismo esquema — esto reduce mucho la probabilidad de discrepancias.
Después puedes añadir comprobaciones de reglas de negocio y métricas como:
- porcentaje de productos sin descripción;
- porcentaje de productos con precio sospechosamente bajo/alto;
- número de duplicados de id o combinaciones repetidas de name + price.
Estas cifras se pueden enviar a un sistema de métricas (Prometheus, Datadog, etc.) y mantener SLO específicos de calidad de datos — igual que defines SLO para el código.
9. Cómo se relacionan la carga y el feed entre sí
A veces parece que «rendimiento» y «calidad de datos» son dos temas poco relacionados. En la práctica están bastante entrelazados.
Ejemplos de vínculos:
- Bajo carga, parte de las solicitudes empieza a seguir «ramas» de lógica poco comunes que antes casi no aparecían. Por ejemplo, productos con tipos especiales de descuentos o shipping no estándar. Si el feed está sucio en esos puntos, puedes acabar con errores y degradación seria del rendimiento (montones de validaciones, excepciones, lógica de fallback).
- Si el feed es muy ruidoso (descripciones enormes con HTML, tags sin sentido), el servidor MCP tiene que arrastrar y serializar más datos; esto afecta directamente al tiempo de procesamiento del tool-call y al tamaño de la respuesta.
- En la parte de comercio, un feed deficiente puede llevar a un gran número de intentos de checkout «vacíos», cuando el usuario elige un producto que resulta estar out of stock. Esto golpea tanto a la UX como a las métricas de ACP (aumento de intents no exitosos).
Es útil verlo como una matriz:
| Problema del feed | Síntoma bajo carga | Dónde mirar |
|---|---|---|
| Precios/monedas inconsistentes | Errores en ACP, pagos rechazados | Logs de ACP + SLO de checkout |
| Productos duplicados | Resultados de recomendación extraños, llamadas de más | Logs de MCP, métricas de UX |
| Faltan imágenes/descripciones | El modelo da recomendaciones «planas» | Logs de la App + feedback de UX |
| HTML/basura en descripciones | Serializaciones lentas, payloads grandes | Latencia de MCP |
La ejecución de carga aquí hace de foco: ayuda a iluminar aquellas partes del feed que en la vida normal se tocaban poco, pero que con tráfico activo empiezan a disparar problemas.
10. Integrarlo en el proceso de lanzamiento de GiftGenius
Desde el punto de vista del proceso, todo lo anterior no debe ser «una vez antes del primer prod». En el plan de estudio de los módulos 16 («Producción, red y escalado») y 17 («Observabilidad y calidad») este enfoque está pensado como parte del checklist regular de lanzamiento: antes de publicar no solo ejecutas unit/contract/E2E, sino también un smoke‑load corto más la verificación del feed.
Pipeline mínimo razonable antes de desplegar una nueva versión:
- Unit + contract + pruebas de integración en verde.
- Smoke‑load corto contra MCP/ACP en staging si se cambió código crítico (lógica de búsqueda, trabajo con la BD, checkout).
- El validador del feed se ejecuta sin errores; las métricas básicas del feed (número de registros rotos, porcentaje sin imágenes, etc.) dentro de límites aceptables.
- Dashboards y alertas actualizados teniendo en cuenta los nuevos endpoints y SLO.
- Plan de rollback preparado en caso de fallo: o desactivar la feature con un flag, o revertir el build.
Así, tu GiftGenius deja de ser «una demo para el DevDay» y se convierte en un servicio listo para vivir en el Store y para picos de tráfico.
11. Errores típicos en pruebas de carga y verificación del feed
Error n.º 1: prueba de carga «a través de ChatGPT» y no contra tu backend.
A veces se intenta «probar todo como en la realidad» y se lanzan scripts que pasan por el UI de ChatGPT. Al final te topas con los límites de OpenAI, quemas tokens y obtienes resultados muy ruidosos. Mientras tanto, los problemas de MCP/ACP podían detectarse cien veces más barato disparando directamente a /mcp y /api/checkout.
Error n.º 2: centrarse solo en el tiempo de respuesta medio.
«Nuestra latencia media es 500 ms, todo genial» — y se olvida que el p95 es de 5 segundos. Ya hablamos en el tema de SLO de que es la cola de la distribución (p95/p99) la que define la UX real. Bajo carga, la media suele mantenerse decente, mientras que la cola crece 2–3 veces.
Error n.º 3: intentar montar una «carga enterprise» en lugar de un smoke‑load práctico.
Pasarse meses desarrollando un entorno complejo que simula decenas de miles de usuarios, para una ChatGPT App del nivel de GiftGenius, casi siempre es innecesario. Es mucho más útil tener un smoke‑load simple pero ejecutado con regularidad sobre 50–100 VU con métricas claras.
Error n.º 4: escenario de carga irrealista.
El script envía la misma solicitud una y otra vez, sin variaciones de usuario, idioma, tipo de producto, y además no toca ACP ni webhooks. Como resultado, pruebas un único happy‑path caliente y los «bordes» reales del sistema quedan en la sombra. Mejor modelar al menos un flujo simplificado pero verosímil: distintos presupuestos, distintos intereses, parte de usuarios llega al checkout, parte — no.
Error n.º 5: verificar el feed «a ojo» o solo en prod.
Se armó el feed, se subió a prod, se vieron recomendaciones extrañas del modelo y comenzó el desconcierto. Un simple script con Zod/JSON Schema habría mostrado en un minuto que el 10% de los productos no tiene imágenes, el 5% tiene precio 0 y el 3% usa la moneda XXX. La ausencia de validación automática del feed es una de las fuentes más habituales de vergüenza en aplicaciones de comercio.
Error n.º 6: confiar en que la LLM «ya lo entenderá» con un feed malo.
Sí, el modelo sabe mucho, pero no inventará un precio correcto ni la disponibilidad. Si el mismo producto aparece en el feed con distintos precios, o «en stock»/«sin stock» al mismo tiempo, el agente puede producir alucinaciones y una experiencia inconsistente. La responsabilidad de la limpieza de los datos es tuya, no del modelo.
Error n.º 7: no vincular las métricas del feed con los SLO generales.
Puedes tener un MCP y un ACP perfectamente rápidos, pero si el 30% de los productos del feed está «roto», la experiencia de usuario será igual de mala. A menudo los equipos siguen solo SLO técnicos (latencia, tasa de errores) e ignoran SLO de calidad de datos (porcentaje mínimo de SKU válidos, máximo de duplicados, etc.). Resultado: «por números todo va bien», pero por sensaciones — no.
Error n.º 8: ejecutar pruebas de carga directamente en el prod sin preparación.
A veces alguien un viernes por la tarde decide «correr rápido k6 en el MCP de producción» sin avisar a nadie. En el mejor de los casos, estropearás métricas reales y desconcertarás al on‑call con un pico de tráfico; en el peor, te toparás con rate limits de un API externo o de la pasarela de pago. Ejecuta siempre los primeros escenarios en staging y, si necesitas probar en prod, hazlo conscientemente, con ventanas y avisos.
GO TO FULL VERSION