1. Por que o ChatGPT App precisa de testes de carga?
Na web clássica, o teste de carga costuma ser associado à imagem “milhões de RPS, um cluster gigante, pizza para o SRE”. Para o ChatGPT App e servidores MCP, a realidade é mais simples e, felizmente, mais barata. Em princípio você já está familiarizado com SLO, mas vamos ver como SLO/observabilidade e a qualidade do feed interagem sob carga.
A principal particularidade: o ChatGPT espera a conclusão de um tool call para continuar a geração da resposta. O usuário vê um stream bonito de tokens, mas assim que o modelo decide chamar uma ferramenta, a magia do stream acaba — até o backend responder. Se seu servidor MCP ou ACP às vezes responde em 8–10 segundos em vez das 2–4 segundos alvo, o UX se transforma de “assistente mágico” em “só mais um site lento”.
Além disso, há um orçamento de timeout rígido: para chamadas de ferramentas, a OpenAI mantém um limite superior na ordem de dezenas de segundos (os números exatos dependem do modo, mas pense em 30–60 segundos, e do ponto de vista de UX — idealmente até 5–10 segundos). Se no pico de carga seus tool calls de repente passam a levar 25–30 segundos, você ainda está formalmente dentro do limite, mas do ponto de vista do usuário já “quebrou”.
Segundo ponto: o que importa não é tanto um RPS abstrato, e sim a concorrência. Para um App vindo da Store, é totalmente realista ter 50–100 usuários ativos simultâneos; é exatamente isso que queremos verificar, e não “se aguenta 50k RPS de um GET /health sintético”.
E por fim, o ChatGPT App é uma stack:
flowchart LR User --> ChatGPT ChatGPT -->|tools/call| MCP["Servidor MCP do GiftGenius"] MCP --> DB["Base com o feed de presentes"] MCP --> ACP["Checkout / backend ACP"] ACP --> PSP["Gateway de pagamento / Stripe"]
Se não verificarmos como essa stack se comporta sob uma carga pequena, mas realista, qualquer campanha promocional ou destaque na Store pode rapidamente transformá‑la em um slide “como não fazer produtos com LLM”.
Nesta aula, por “testes de carga leves” vamos entender execuções curtas (geralmente 1–10 minutos) que verificam:
- se o sistema aguenta o pico de usuários esperado;
- se o p95/p99 de latência não sai do SLO;
- se não aparecem erros, timeouts e rate limits de APIs externas.
E em paralelo veremos o outro lado da qualidade — os dados do feed de produtos (product feed, a seguir apenas “feed”), sem os quais nenhum GiftGenius será “Gift” nem “Genius”.
Primeiro vamos nos entender com testes de carga leves para MCP/ACP (o que exatamente e como carregar, quais métricas observar), depois vamos aterrissar isso em observabilidade (latência, erros, recursos, webhooks e logs) e, na segunda metade, falar sobre a qualidade do feed e como ela surpreendentemente falha sob carga.
2. O que carregar: não o ChatGPT, e sim suas APIs
É importante fixar uma ideia para não confundir depois: o teste de carga é conduzido diretamente no nosso backend — servidor MCP, endpoints ACP, webhooks — e não via a UI do ChatGPT.
Os motivos são vários.
- Primeiro, economia. Se você disparar tool calls reais via ChatGPT, vai pagar por tokens e, ao mesmo tempo, esbarrar nos limites do ChatGPT, embora esteja testando seu próprio código.
- Segundo, previsibilidade. Em chamadas diretas a /mcp ou /api/checkout você controla o cenário, sem depender da decisão do modelo sobre chamar ou não a ferramenta naquele momento.
- Terceiro, transparência. Sob carga, você quer ver claramente: aqui 2000 requisições ao MCP em 5 minutos, aqui a distribuição de latência, aqui o gráfico de CPU. Se você passar a carga pelo ChatGPT, uma camada adicional de ruído e restrições só vai complicar o cenário.
Conjunto típico de endpoints para o teste de carga do GiftGenius:
- o endpoint do servidor MCP que implementa ferramentas JSON‑RPC (/mcp ou similar);
- um ou dois endpoints ACP para criar e finalizar o checkout (em modo sandbox do processador de pagamentos);
- talvez — o endpoint que processa webhooks do processador de pagamentos, para ver como ele se comporta no pico de eventos.
Vamos considerar que temos um backend Next.js 16, onde roda o servidor MCP acessível em /api/mcp, e um servidor ACP com o endpoint /api/checkout/create.
3. Mini‑cenário de smoke‑load para o GiftGenius
Imagine que nossos product managers acreditam em um futuro brilhante e dizem: “O pico realista — 50 usuários simultâneos; cada um entra, escolhe um presente e às vezes chega ao pagamento”.
Para um teste de carga leve, basta modelarmos, digamos, 30–50 “usuários virtuais” (VU), cada um executando a sequência:
- Chamada da ferramenta giftgenius.search_gifts (busca de presentes por perfil e orçamento).
- Chamada de giftgenius.get_gift_details para dois itens do resultado.
- (Às vezes) chamada ao endpoint ACP create_checkout_session para um item.
Tudo isso diretamente via HTTP ao nosso MCP/ACP, sem ChatGPT.
Chamada JSON‑RPC para o MCP
Exemplo de corpo de requisição ao 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"],
},
},
};
No projeto real, a estrutura pode variar um pouco, mas o princípio é o mesmo: um método JSON‑RPC, dentro dele — a ferramenta e os argumentos.
4. Escrevendo um script de carga simples em TypeScript
Como primeiro passo, vamos implementar a parte mais simples do nosso cenário — a chamada giftgenius.search_gifts ao MCP. Primeiro, faremos um script Node.js mínimo em TypeScript que envia essas requisições a /api/mcp e mede a latência, e depois adicionamos checkout e caminhos mais complexos.
Cliente HTTP básico
Suponha que temos um .env com 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;
}
Aqui também dá para adicionar um parse simples do JSON de resposta, mas para fins de latência/error rate isso basta.
Execução concorrente de múltiplas requisições
Precisamos controlar a quantidade de requisições simultâneas. Por simplicidade, vamos usar um número fixo de “usuários virtuais” e pedir que cada um faça N requisições em sequência.
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); // marcar como erro
}
}
return latencies;
}
Agora podemos executar, por exemplo, 20 desses usuários virtuais:
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));
Isso já garante aproximadamente 200 chamadas ao MCP, parte delas executadas em paralelo, ou seja, com concorrência suficientemente alta.
Cálculo de p95 e error rate
Vamos adicionar um utilitário pequeno para calcular percentil e erros. Lembrete: p95 é o valor abaixo do qual 95% das requisições ficam.
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;
}
E no main adicionamos a saída:
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)}%`);
Agora você tem um script mínimo de smoke‑load que pode ser executado localmente ou em staging antes do release. Com isso, você não toca no ChatGPT, não queima tokens, e toda a atenção fica no seu MCP.
O que fazer com o ACP e o checkout
De forma análoga, você pode adicionar outro helper callCreateCheckoutSession que vai bater no endpoint do ACP. Aqui é importante usar o modo de testes/sandbox do processador de pagamentos, para não inflar pedidos reais. Uma chamada típica será um POST com 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;
}
Em seguida, você pode fazer no runVirtualUser o padrão: 3 vezes busca → 1 vez checkout, para simular o funil “há mais buscas do que compras”.
5. Ferramentas mais sérias: k6 (mas do jeito simples)
O script em Node é ótimo como “entrada mínima”, mas às vezes é conveniente usar uma ferramenta especializada, como o k6, onde os cenários são escritos em JavaScript, e o runtime é em Go (ou seja, rápido).
Exemplo de um pequeno script k6 para o 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 execução:
MCP_URL=http://localhost:3000/api/mcp k6 run loadtest-mcp.js
O k6 calcula por conta própria p95/p99 e error rate, gera relatórios bonitos — e depois você pode exportá‑los para o Grafana e outros sistemas.
Importante: mesmo com essas ferramentas, nosso objetivo continua o mesmo — não suportar um milhão de RPS, e sim garantir que, com 5–10× o pico esperado, o sistema não desmorona e o p95 permanece dentro do SLO.
6. O que observar durante (e depois de) o ensaio de carga
Já discutimos métricas e SLO; agora vamos apenas “aterrissar” isso no contexto de carga.
Primeiro, latência. Para ferramentas do MCP como search_gifts, você já havia definido um objetivo do tipo “p95 < 2–3 segundos”. Durante o smoke‑load, veja se o p95/p99 não disparou 2–3 vezes. É importante comparar com o baseline: se antes da mudança de código o p95 era 400 ms e depois passou a 1500 ms, mesmo que ainda esteja formalmente dentro do SLO, já é motivo para reflexão.
Segundo, error rate. Sob carga, costumam aparecer coisas inesperadas: esgotamento do pool de conexões do BD, respostas 429 inesperadas de uma API externa, timeouts ao chamar o processador de pagamentos. Em carga normal, o error rate deve ser próximo de zero; em smoke‑load, falhas pontuais são aceitáveis, mas certamente não 5–10 %.
Terceiro, métricas de recursos: CPU, memória, às vezes — quantidade de descritores de arquivos e conexões abertas. Isso depende da sua infraestrutura, mas a ideia central é simples: você não quer ver, com 30 VU, a CPU a 100 % e o GC consumindo metade do tempo.
Quarto, webhooks. Se você tem um cenário de comércio, o ponto final de um pedido frequentemente depende do processamento bem‑sucedido de um webhook do sistema de pagamentos. É importante observar não apenas a velocidade da requisição no ACP, mas também a latência “o webhook chegou → nós o processamos com sucesso”.
E por fim, logs. Logs estruturados com trace_id/checkout_session_id permitem, após o ensaio de carga, pegar um ou dois dos requests mais lentos ou com erro e seguir a cadeia: MCP → API externa → ACP → webhook. Isso é especialmente útil se, sob carga, você vê caudas estranhas no p99.
7. Qualidade dos dados do feed: da estrutura ao sentido
Vimos como latência, erros e recursos se comportam sob carga. Mas mesmo que você esteja dentro dos SLOs nesses pontos, a experiência do usuário ainda pode “desmoronar” por causa de dados ruins.
Vamos ao segundo grande tema: dados. Em um App de comércio como o GiftGenius, o product feed (feed de produtos) não é “algo em disco”, e sim literalmente o combustível para a LLM e os agentes. Se o feed é lixo, o modelo não “vai inventar” por você o preço e a disponibilidade.
É útil pensar na qualidade do feed em três camadas.
Nível estrutural
É a validade básica dos dados:
- O JSON faz parse corretamente.
- Todos os campos obrigatórios estão presentes: id, name, price, currency, imageUrl, availability, etc.
- Os tipos de valores correspondem ao esperado: preço — número, availability — enum, categories — array de strings.
- Sem duplicatas de id.
Você já cobriu parte disso com testes de contrato, ao descrever o JSON Schema/Zod‑schema do feed. Agora é preciso aplicar esses schemas a volumes reais de dados.
Exemplo de um Zod‑schema simples para um item do feed do 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([]),
});
E o schema do feed como um todo — apenas z.array(giftItemSchema).
Nível de negócio (semântica)
Estruturalmente, o item pode ser válido, mas do ponto de vista de negócio — absurdo:
- Preço 0 ou 0,01 para um produto caro.
- Moeda não correspondente ao mercado (USD para produtos vendidos apenas em EUR).
- inStock = true, mas a data da última atualização foi há meio ano.
- Categorias com 1000 variantes sem padronização.
Para esse nível, é útil adicionar verificações extras e “regras de bom senso”. Por exemplo:
const businessRules = (item: GiftItem) => {
const problems: string[] = [];
if (item.price > 10000) {
problems.push("preço suspeitamente alto");
}
if (!item.inStock && item.tags.includes("bestseller")) {
problems.push("bestseller, mas indisponível");
}
return problems;
};
Essas verificações podem ser executadas como parte de um job noturno ou ao gerar um novo feed.
Nível LLM
O modelo é muito inteligente, mas tem suas “manias”:
- Descrição cheia de HTML, tags desnecessárias e texto técnico.
- Idiomas misturados (meio feed em russo, meio em inglês) sem indicação de locale.
- Nomes “SEO” muito longos no estilo “Comprar o melhor super presente agora barato”.
Nesse nível, é importante transformar os dados para um formato amigável:
- Remover tags HTML ou convertê‑las para texto puro.
- Normalizar o idioma das descrições (ou ao menos indicar explicitamente o locale).
- Podar nomes excessivamente longos e informações duplicadas.
Essas tarefas podem ser parcialmente automatizadas (por exemplo, via scripts de pré‑processamento) e, parcialmente, resolvidas em acordo com a equipe que abastece o feed.
8. Prática: validador do feed para o GiftGenius
Vamos adicionar ao nosso projeto um script simples validateFeed.ts, que lê o JSON do feed, valida com Zod e calcula métricas básicas de qualidade.
// 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 produtos: ${items.length}`);
const missingImages = items.filter(i => !i.imageUrl).length;
console.log(`Sem imagens: ${missingImages}`);
}
main().catch((e) => {
console.error("Feed validation failed:", e);
process.exit(1);
});
Aqui usamos o mesmo contrato que o servidor MCP, ou seja, os testes de contrato e a verificação do feed usam o mesmo schema — isso reduz bastante a probabilidade de divergências.
Depois, você pode adicionar verificações de regras de negócio e métricas como:
- percentual de itens sem descrição;
- percentual de itens com preço suspeitamente baixo/alto;
- quantidade de duplicatas de id ou repetições de name + price.
Esses números já podem ser enviados para um sistema de métricas (Prometheus, Datadog, etc.) e ter SLOs próprios de qualidade de dados — assim como você define SLOs para código.
9. Como carga e feed se relacionam
Às vezes, parece que “desempenho” e “qualidade de dados” são dois temas pouco relacionados. Na prática, eles se entrelaçam bastante.
Alguns exemplos de ligação:
- Sob carga, parte das requisições começa a percorrer “ramificações raras” da lógica, antes quase nunca utilizadas. Por exemplo, produtos com tipos especiais de desconto ou shipping não padrão. Se o feed estiver sujo nesses pontos, você pode ter tanto erros quanto degradação séria de desempenho (um monte de validações, exceções, lógica de fallback).
- Se o feed tiver muito ruído (descrições enormes com HTML, tags sem sentido), o servidor MCP precisa buscar e serializar mais dados; isso impacta diretamente o tempo de processamento do tool-call e o tamanho da resposta.
- Na parte de commerce, um feed ruim pode levar a muitas tentativas de checkout “vazias”, quando o usuário escolhe um item que, de repente, está fora de estoque. Isso afeta o UX e as métricas do ACP (aumento de intents malsucedidos).
É útil ver isso como uma matriz:
| Problema do feed | Sintoma sob carga | Onde observar |
|---|---|---|
| Preços/moedas inconsistentes | Erros no ACP, pagamentos rejeitados | Logs do ACP + SLO de checkout |
| Duplicatas de produtos | Resultados de recomendação estranhos, chamadas extras | Logs do MCP, métricas de UX |
| Imagens/descrições ausentes | O modelo fornece recomendações “sem graça” | Logs do App + feedback de UX |
| HTML/lixo nas descrições | Serializações lentas, payloads grandes | Latência do MCP |
O ensaio de carga aqui funciona como uma lanterna: ajuda a destacar aquelas partes do feed que, no dia a dia, raramente eram tocadas, mas que com tráfego ativo começam a falhar.
10. Incorporando isso ao processo de release do GiftGenius
Do ponto de vista de processo, tudo o que foi descrito acima não deve ser “uma vez antes do primeiro prod”. No plano dos módulos 16 (“Produção, rede e escalabilidade”) e 17 (“Observabilidade e qualidade”), essa abordagem está embutida exatamente como parte do checklist regular de release: antes do release você roda não apenas unit/contract/E2E, mas também um smoke‑load curto mais a verificação do feed.
Pipeline mínimo razoável antes de publicar uma nova versão:
- Unit + contract + testes de integração passando.
- Smoke‑load curto contra MCP/ACP em staging, se o código crítico mudou (lógica de busca, trabalho com BD, checkout).
- O validador do feed roda sem erros; as métricas básicas do feed (quantidade de registros quebrados, percentual sem imagens, etc.) estão dentro dos limites aceitáveis.
- Dashboards e alertas atualizados levando em conta os novos endpoints e SLOs.
- Em caso de falha, um plano de rollback está pronto: ou desativação da feature por flag, ou rollback do build.
Assim, seu GiftGenius deixa de ser “uma demo para o DevDay” e se transforma em um serviço pronto para a vida na Store e para picos de tráfego.
11. Erros comuns em testes de carga e verificação do feed
Erro nº 1: teste de carga “via ChatGPT”, e não no seu backend.
Às vezes, tentam “testar tudo como na realidade” e rodam scripts que passam pela UI do ChatGPT. No fim, esbarram nos limites da OpenAI, queimam tokens e obtêm resultados extremamente ruidosos. Enquanto isso, os problemas do MCP/ACP poderiam ser detectados muito mais barato, atirando diretamente em /mcp e /api/checkout.
Erro nº 2: foco apenas no tempo médio de resposta.
“Temos latência média de 500 ms, tudo ótimo” — e o fato de o p95 ser 5 segundos é convenientemente esquecido. Já discutimos em SLO que é a cauda da distribuição (p95/p99) que define o UX real. Sob carga, a média muitas vezes permanece decente, enquanto a cauda cresce duas ou três vezes.
Erro nº 3: tentar criar uma “carga enterprise” em vez de um smoke‑load prático.
Passar meses desenvolvendo um ambiente complexo que imita dezenas de milhares de usuários, para um ChatGPT App do nível do GiftGenius, quase sempre é exagero. Muito mais útil é ter um smoke‑load simples, mas executado regularmente, com 50–100 VU e métricas claras.
Erro nº 4: cenário de carga irrealista.
O script envia a mesma requisição, sem variação de usuário, idioma, tipo de produto, e não toca o ACP e os webhooks. No fim, você testa um único happy path quente, enquanto os “cantos” reais do sistema continuam nas sombras. É melhor modelar ao menos um fluxo simplificado, mas verossímil: orçamentos diferentes, interesses diferentes, parte dos usuários chega ao checkout, parte não.
Erro nº 5: verificar o feed apenas “no olho” ou direto em prod.
O feed foi montado, publicado em prod, o modelo começou a dar recomendações estranhas e a equipe coçou a cabeça. Enquanto isso, um script simples em Zod/JSON Schema poderia ter mostrado em um minuto que 10% dos itens não têm imagens, 5% têm preço 0 e 3% usam moeda XXX. A ausência de validação automática do feed é uma das fontes mais frequentes de vergonha em apps de comércio.
Erro nº 6: esperar que a LLM “entenda tudo sozinha” com um feed ruim.
Sim, o modelo sabe muito, mas não vai inventar preço ou disponibilidade corretos. Se o mesmo produto aparece no feed com preços diferentes, ou “em estoque”/“sem estoque” ao mesmo tempo, o agente pode produzir tanto alucinações quanto uma experiência inconsistente para o usuário. A responsabilidade pela limpeza dos dados é sua, não do modelo.
Erro nº 7: falta de ligação entre métricas do feed e SLOs gerais.
Você pode ter MCP e ACP impecavelmente rápidos, mas se 30% dos itens do feed estiverem “quebrados”, a experiência do usuário ainda será péssima. Com frequência, as equipes monitoram apenas SLOs técnicos (latência, error rate) e ignoram SLOs de qualidade de dados (percentual mínimo de SKUs válidos, máximo de duplicatas, etc.). Resultado: “pelos números está tudo bem”, mas a sensação prática é o oposto.
Erro nº 8: rodar testes de carga diretamente no prod, sem preparação.
Às vezes, alguém decide numa sexta‑feira à noite “rodar rapidinho o k6 no MCP de produção”, sem avisar ninguém. No melhor cenário, você vai bagunçar as métricas reais e confundir o engenheiro de plantão com o pico de tráfego; no pior — vai esbarrar em rate limits de uma API externa ou do sistema de pagamentos. Sempre rode os primeiros cenários em staging e, se precisar testar em prod, faça isso de forma consciente, com janelas e notificações.
GO TO FULL VERSION