1. Por que pensar em “resiliência” no ChatGPT App
Em um aplicativo web comum, o usuário ao menos vê a URL, o spinner do navegador e pode atualizar a página. No ChatGPT o usuário vê uma única tela: o chat e seu App. Se algo fica lento, ele não distingue quem é o culpado — OpenAI, seu Gateway, o sistema de pagamentos ou o microserviço de analytics do vizinho. Para ele, tudo isso é “ChatGPT + seu App”.
Quando um tool-call fica pendurado por 30–60 segundos, o modelo espera, espera… e, no melhor cenário, pede desculpas pelo atraso. No pior — alucina uma resposta em vez de usar os dados do seu backend. Portanto, resiliência não é só sobre SRE e uptime; é também sobre a qualidade da resposta, o tom do modelo e as métricas na Store.
No ecossistema de ChatGPT App temos vários circuitos independentes:
- ChatGPT ↔ MCP Gateway.
- Gateway ↔ seus serviços backend/REST (Gift REST API, Commerce REST API, Analytics Service etc.).
- Seus serviços ↔ APIs externas (LLM, pagamentos, catálogos).
- Webhooks de entrada (ACP, Stripe, quaisquer integrações) ↔ seus handlers.
O problema é que uma falha em um ponto pode desencadear um efeito cascata: o Gateway espera honestamente um serviço travado, os workers saturam, as conexões acabam, os clientes começam a fazer retries e, em poucos minutos, você tem um colapso total: tudo pega fogo e afunda ao mesmo tempo. É justamente disso que nos protegem os quatro padrões de que falaremos hoje:
- Timeouts — nunca esperamos para sempre.
- Circuit breaker — não ficamos batendo em porta fechada.
- Bulkheads — construímos “compartimentos” e não deixamos o navio inteiro afundar.
- Proteção contra tempestades de webhooks — aceitamos que webhooks vêm com duplicatas, picos e retries, e nos preparamos.
2. Timeouts: não esperamos para sempre
O que é timeout e por que tudo fica ruim sem ele
Timeout é o tempo máximo que seu código está disposto a esperar por uma dependência: banco de dados, servidor MCP, HTTP API externa, modelo. Se a resposta não chegar no tempo definido — consideramos a chamada malsucedida, liberamos recursos e retornamos um erro compreensível ou um fallback.
Sem timeouts, as requisições podem:
- ficar penduradas indefinidamente,
- tomar conexões e o pool de threads,
- bloquear requisições subsequentes,
- provocar falhas em cascata.
O padrão é simples: “melhor uma falha previsível em 3–5 segundos do que um silêncio incompreensível por 5 minutos”.
É importante lembrar que temos timeouts em vários níveis:
- no nível do proxy/balanceador (Cloudflare, Nginx),
- no nível do MCP Gateway (clientes HTTP para microserviços),
- nos próprios serviços (chamadas ao BD, APIs externas, LLM).
Para o ChatGPT, de forma geral, faz sentido buscar um tempo total de tool-call na faixa de 5–10 segundos para operações comuns e no máximo 20–30 segundos para as mais pesadas. Qualquer coisa acima disso — quase certamente um UX ruim.
fetchWithTimeout simples em TypeScript
Vamos à prática. No GiftGenius MCP Gateway temos um cliente HTTP auxiliar, que chama o selecionador de presentes, o serviço de commerce e analytics. Vamos envolver o fetch padrão em uma função com timeout:
// src/gateway/httpClient.ts
export async function fetchWithTimeout(
url: string,
opts: RequestInit & { timeoutMs?: number } = {}
) {
const { timeoutMs = 5000, ...rest } = opts;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, { ...rest, signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}
}
Agora, no código do Gateway, nunca fazemos um fetch “cru”, apenas via esse helper:
// src/gateway/giftClient.ts
import { fetchWithTimeout } from "./httpClient";
export async function callGiftService(path: string) {
const res = await fetchWithTimeout(
process.env.GIFT_SERVICE_URL + path,
{ timeoutMs: 4000 }
);
if (!res.ok) {
throw new Error(`gift_service_${res.status}`);
}
return res.json();
}
Essa abordagem garante que, mesmo que o serviço de presentes trave, em 4 segundos interromperemos a conexão e conseguiremos retornar um erro MCP ao ChatGPT, em vez de manter a conexão até o fim.
Onde exatamente colocar timeouts no GiftGenius
No nosso exemplo, GiftGenius:
- No nível do Gateway: timeouts para chamadas ao Gift REST API, Commerce REST API, Analytics Service / REST API.
- Dentro desses serviços: timeouts para chamadas ao BD, ACP/sistemas de pagamento, APIs externas de recomendação.
- Na entrada do Gateway: timeout geral da requisição do ChatGPT, para que o tool-call não vire um “spinner infinito”.
É importante que o tempo de espera no nível superior seja um pouco maior que nos internos. Por exemplo, se o Gateway espera o backend por 5 segundos e o backend espera o BD por 3 segundos, temos uma folga para processar e serializar o resultado.
Como explicar timeouts para o modelo do ChatGPT
Para o ChatGPT, é importante retornar erros semânticos, e não derrubar silenciosamente as conexões. Em vez de um 500 genérico, é melhor retornar um erro MCP estruturado que o modelo consiga comunicar ao usuário: “O serviço de recomendação de presentes está sobrecarregado agora, tente novamente mais tarde” e assim por diante.
Isso significa que, no Gateway, em caso de timeout, você deve:
- Capturar AbortError ou nosso timeout_….
- Formar uma resposta MCP com um código significativo e uma descrição curta.
- Dar ao modelo a chance de decidir como explicar isso à pessoa.
Timeouts resolvem o problema de requisições penduradas, mas, se a dependência começou a falhar em massa, eles não nos salvam de uma avalanche de tentativas idênticas e malsucedidas. Aqui precisamos do próximo nível de proteção — o circuit breaker.
3. Circuit breaker: “disjuntor” contra serviços morrendo
Intuição: por que apenas o timeout não basta
Já aprendemos a limitar o tempo de espera de chamadas individuais com timeouts. O timeout protege uma chamada específica. Mas, se a dependência “morreu de vez” (por exemplo, o serviço de commerce cai por OOM — Out Of Memory — em toda requisição), continuaremos a chamá-la, esperando 3–5 segundos a cada vez, capturando erro, carregando rede e CPU e, de novo, esperando.
O circuit breaker (disjuntor) adiciona memória: ele monitora erros e timeouts e, quando passam de um limite, para de enviar requisições para esse serviço. Em vez disso, retorna falha rápida ou um fallback. Depois de algum tempo, ele tenta novamente com cuidado no modo half-open.
Estados clássicos do disjuntor:
- Closed — tudo normal, as requisições passam.
- Open — o serviço é considerado “fora”, requisições não passam, erro imediato.
- Half-open — testamos um número limitado de requisições; se tiverem sucesso — voltamos a closed, se falharem — voltamos a open.
Esquema simples de circuit breaker
Um pequeno diagrama:
stateDiagram-v2
[*] --> Closed
Closed --> Open: erros demais
Open --> HalfOpen: cooldown expirado
HalfOpen --> Closed: alguns sucessos em sequência
HalfOpen --> Open: erros novamente
Open --> Open: falha rápida
Mini-implementação de circuit breaker em TypeScript
Em produção, normalmente usa-se bibliotecas prontas (para Node.js há, por exemplo, opossum ou soluções leves feitas em casa), mas, para entender a mecânica, basta uma classe compacta.
Exemplo de um breaker bastante simplificado em volta da chamada ao módulo de commerce:
// src/gateway/circuitBreaker.ts
type State = "closed" | "open" | "half-open";
export class CircuitBreaker {
private state: State = "closed";
private failureCount = 0;
private nextAttemptAt = 0;
constructor(
private readonly failureThreshold = 5,
private readonly cooldownMs = 30_000
) {}
async call<T>(fn: () => Promise<T>): Promise<T> {
const now = Date.now();
if (this.state === "open") {
if (now < this.nextAttemptAt) {
throw new Error("circuit_open");
}
this.state = "half-open";
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
throw err;
}
}
private onSuccess() {
this.failureCount = 0;
this.state = "closed";
}
private onFailure() {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = "open";
this.nextAttemptAt = Date.now() + this.cooldownMs;
}
}
}
E uso no cliente do serviço de commerce:
// src/gateway/commerceClient.ts
const commerceBreaker = new CircuitBreaker(3, 20_000);
export async function callCommerce(path: string) {
return commerceBreaker.call(async () => {
const res = await fetchWithTimeout(
process.env.COMMERCE_URL + path,
{ timeoutMs: 3000 }
);
if (!res.ok) throw new Error(`commerce_${res.status}`);
return res.json();
});
}
Aqui, quando o commerce começa a responder com erros em massa ou não consegue cumprir o timeout, após algumas falhas o breaker passa a open. Nesse estado, durante o cooldownMs, nem tentamos chamar o serviço e retornamos imediatamente o erro circuit_open.
O que o ChatGPT deve ver quando o breaker “derruba” um serviço
Do ponto de vista do ChatGPT, é melhor se você:
- Responde rapidamente com um erro MCP “commerce_unavailable” ou “gift_service_overloaded”.
- Adiciona uma descrição clara: “O serviço de pagamento está temporariamente indisponível, vamos tentar mais tarde”.
- Não esconde o erro atrás de retries infinitos.
Este é exatamente o caso em que a “falha rápida e honesta” é melhor que ficar pendurado por muito tempo. Especialmente no checkout: o usuário aceita melhor uma mensagem clara do que ficar 40 segundos olhando um spinner e receber um “algo deu errado”.
Timeouts e breaker nos protegem de dependências “ruins” ou fora do ar, mas não resolvem o problema quando um tipo de carga consome todos os recursos e começa a sufocar o restante do sistema. Para isso, precisamos de mais uma camada — bulkheads.
4. Bulkheads: isolar “compartimentos” para que um não afunde o navio inteiro
Analogia com um navio
O padrão bulkhead tem o nome das anteparas de um navio: se há um buraco em um compartimento, a água não se espalha pelo navio todo. Em arquitetura, isso significa: dividir recursos entre diferentes linhas de trabalho, para que um serviço sobrecarregado não consuma tudo — CPU, conexões, pools — e não derrube caminhos críticos.
Em microsserviços, isso geralmente é feito com:
- pools de conexões HTTP separados,
- pools de threads/workers,
- filas/tópicos,
- até clusters de BD separados para operações críticas.
A ideia é que, se o serviço de recomendações de presentes começar a ficar lento e travar, ele vai esgotar apenas os próprios recursos, mas não vai quebrar o checkout e a autenticação.
Bulkheads no mundo Node.js e no MCP Gateway
No Node.js, não temos threads no sentido clássico (há event loop e workers), mas podemos limitar o número de tarefas paralelas para cada direção.
Exemplo: no Gateway há três dependências externas:
- Serviço de presentes (recomendação, chamadas LLM pesadas).
- Serviço de commerce (checkout, ACP).
- Serviço de analytics (log de eventos).
Podemos introduzir limites simples para requisições simultâneas a cada um deles.
Por exemplo, um pequeno “semáforo” para limitar a paralelização:
// src/gateway/bulkhead.ts
export class Bulkhead {
private active = 0;
private queue: (() => void)[] = [];
constructor(private readonly maxConcurrent: number) {}
async run<T>(fn: () => Promise<T>): Promise<T> {
if (this.active >= this.maxConcurrent) {
await new Promise<void>((resolve) => this.queue.push(resolve));
}
this.active++;
try {
return await fn();
} finally {
this.active--;
const next = this.queue.shift();
if (next) next();
}
}
}
E uso para os serviços:
// src/gateway/clients.ts
import { Bulkhead } from "./bulkhead";
const giftBulkhead = new Bulkhead(10); // até 10 em paralelo
const commerceBulkhead = new Bulkhead(3); // checkout é bem limitado
const analyticsBulkhead = new Bulkhead(50); // pode muitos
export async function callGiftWithBulkhead(fn: () => Promise<any>) {
return giftBulkhead.run(fn);
}
export async function callCommerceWithBulkhead(fn: () => Promise<any>) {
return commerceBulkhead.run(fn);
}
Assim, mesmo que o GPT decida pedir em massa “faça 30 recomendações de presentes complexas”, elas serão executadas no máximo 10 por vez, enquanto o checkout continuará funcionando com seu limite separado.
GiftGenius: quais compartimentos queremos
No GiftGenius, faz sentido ter compartimentos separados para:
- Recomendação de presentes (pesado em LLM, menos crítico, pode ser desacelerado).
- Checkout/ACP (super crítico, precisa de proteção máxima).
- Analytics/logs (importante, mas pode tolerar alguma latência).
Em uma arquitetura mais avançada, você ainda faria o deploy como clusters diferentes com recursos próprios, mas, no escopo desta aula, o importante é a ideia: não deixar funcionalidades secundárias “consumirem todo o oxigênio”.
Esses três padrões — timeouts, circuit breaker e bulkheads — dizem respeito a como você chama para fora, as suas dependências. Mas há outra classe de ameaças à resiliência: fluxos de eventos de entrada, que podem derrubar você mesmo com as chamadas de saída perfeitamente ajustadas. O exemplo mais típico — tempestades de webhooks.
5. Tempestades de webhooks: quando o mundo envia eventos mais rápido do que você aguenta
Como os webhooks se comportam na prática
A quarta fonte de problemas de resiliência — eventos de entrada: webhooks de ACP, Stripe e outros sistemas. São eles que podem causar uma verdadeira “tempestade”, mesmo se você já tiver timeouts, circuit breakers e bulkheads configurados.
Webhooks não são requisições HTTP “on-demand”, mas eventos “push” de sistemas externos (Stripe, ACP, lojas externas etc.). Eles têm algumas características desagradáveis:
- Entrega pelo menos uma vez (at-least-once) — logo, duplicatas são inevitáveis.
- A ordem de entrega não é garantida.
- Em caso de erro, eles tendem a fazer retries: primeiro após um segundo, depois após 10, depois após um minuto… até receberem 2xx.
- Em picos (por exemplo, em promoções), chegam em lotes, criando uma “tempestade”.
Se o seu handler não é idempotente e demora demais, ele vira gargalo, toda a fila entope e os retries apenas intensificam a tempestade. Como resultado, você pode derrubar o banco, a fila, os pools de workers — e, em cadeia, o resto do sistema.
Princípios básicos de proteção contra tempestades
Há algumas ideias que aumentam muito as chances de sobreviver a uma tempestade:
Primeiro, queue-first, process-later. Idealmente, um webhook de entrada não deve executar trabalho pesado de forma síncrona. Em vez disso, ele valida assinatura/formato o mais rápido possível, coloca a tarefa em uma fila e responde 200 OK. O processamento ocorre de forma assíncrona em um worker. Se você precisa de uma “confirmação rápida” para o ChatGPT, pode manter um circuito separado de notificações.
Segundo, idempotência do handler. Um webhook repetido para a mesma operação não deve “criar o pedido de novo” ou “cobrar duas vezes”. Normalmente, isso se resolve armazenando uma idempotency key ou eventId e verificando se já processamos esse evento.
Terceiro, rate limiting e circuit breaker no receptor. Mesmo que o remetente esteja “tempestuoso”, você pode:
- limitar RPS por IP/assinatura/endpoint,
- responder temporariamente 429 ou 503 para reduzir os retries,
- usar breaker para não despejar o fluxo em um downstream quebrado (por exemplo, o BD de pedidos).
Exemplo de handler de webhook em Next.js no GiftGenius
Imagine que temos um ACP/sistema de pagamentos que envia um webhook de status de pedido para POST /api/commerce/webhook. Queremos:
- aceitar o evento rapidamente e colocá-lo em uma fila,
- não processá-lo de forma síncrona,
- não quebrar por causa de duplicatas.
Exemplo simplificado (sem verificação de assinatura e sem fila real — isso ficará nos módulos de segurança e filas):
// app/api/commerce/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
// Aqui poderíamos ter Redis/queue; por enquanto, simulamos com um array
const inMemoryQueue: any[] = [];
const processedEvents = new Set<string>(); // idempotência (para demo)
export async function POST(req: NextRequest) {
const event = await req.json();
const eventId = event.id as string;
if (processedEvents.has(eventId)) {
return NextResponse.json({ ok: true, duplicate: true });
}
// Na prática, aqui haverá verificação de assinatura e de schema
inMemoryQueue.push(event); // enfileiramos para processamento em background
// Um worker em background processará depois e marcará o ID como processado
return NextResponse.json({ ok: true });
}
Por enquanto é uma pseudo-implementação, mas dois pontos são importantes:
- A parte síncrona é a mais leve possível.
- Preparamos a idempotência em torno de event.id.
Na vida real, você vai:
- usar uma fila externa (SQS, RabbitMQ, Kafka),
- armazenar eventos já processados no BD,
- validar a assinatura do webhook e a versão do payload,
- talvez aplicar um Bulkhead/Breaker separado ao redor do handler.
Como isso fica no contexto do GiftGenius
Para o GiftGenius, integrado com ACP/Stripe via webhooks, a proteção contra tempestades é especialmente importante em épocas de pico (Ano Novo, Black Friday). Há muitos eventos:
- criação de intents,
- confirmações de pagamento,
- cancelamentos,
- reembolsos.
Se o seu handler começa a “se alongar” (por exemplo, por causa de chamadas a APIs externas), você corre o risco de:
- o ACP começar a fazer retries,
- os eventos chegarem em lotes,
- o BD de pedidos e o pool de workers ficarem congestionados.
O padrão “queue first” + idempotência + rate limiting na entrada serve justamente como um seguro contra esses cenários.
6. Como esses padrões funcionam juntos
Agora vamos juntar todos esses padrões em um cenário e ver como funcionam no fluxo real “Recomendar presente e finalizar pedido”.
Considere a cadeia “ChatGPT → Gateway → Gift Service → Commerce → webhooks” no cenário:
O usuário diz no chat: “Escolha um presente e já finalize o pedido”.
- O modelo decide chamar seu tool suggest_and_checkout.
- O Gateway chama o serviço de presentes via fetchWithTimeout e o bulkhead do serviço de presentes.
- Se o serviço de presentes travar — ocorre timeout; o breaker ao redor dele, após um certo número de erros, entra em open, e as próximas requisições recebem imediatamente um erro MCP “gift_service_unavailable”.
- Se o serviço de presentes responde, o Gateway chama o serviço de commerce (novamente com timeout e bulkhead separado).
- Quaisquer problemas com o commerce disparam um circuit breaker separado, configurado de forma mais rígida que o do serviço de presentes (porque o checkout é crítico).
- Um pedido bem-sucedido leva a um webhook do ACP para o seu /api/commerce/webhook, que coloca o evento na fila e responde rápido; workers em background processam o pagamento, e webhooks repetidos com o mesmo eventId são ignorados como duplicatas.
Como resultado:
- Um serviço de recomendação pendurado não derruba o checkout.
- Um commerce pendurado não transforma todos os tool-calls em um spinner de um minuto — o ChatGPT recebe rapidamente um erro significativo.
- Tempestades de webhooks não quebram seu circuito HTTP principal.
- Você controla os pontos de degradação: é melhor desativar temporariamente recomendações personalizadas do que derrubar pagamentos.
7. Um pequeno checklist prático para o seu App (em formato narrativo)
Em resumo, em um ChatGPT App típico com MCP/Gateway, faz sentido percorrer, na ordem, as seguintes questões.
Primeiro, verifique se há timeouts em todas as chamadas externas. Todo o código com fetch, requisições ao BD e ao LLM deve usar um wrapper como fetchWithTimeout com valores adequados. É importante que não existam pontos onde a requisição possa ficar pendurada indefinidamente.
Depois, identifique as dependências mais frágeis. Em geral, são sistemas de pagamento, ACP, grandes APIs externas e, às vezes, o próprio BD de pedidos. Em volta deles, vale adicionar um circuit breaker para se proteger de uma avalanche de repetições para um serviço claramente fora. Ao mesmo tempo, defina de antemão como o ChatGPT deve se comportar quando o breaker estiver em estado open.
Em seguida, olhe para seus recursos como “compartimentos”. Tudo passa por um único connection pool e um único pool de workers, ou operações críticas (login, checkout) têm limites próprios de paralelismo, independentes do serviço de recomendações e de analytics? Se não — adicione uma implementação simples de bulkheads, ao menos como um limite bruto de tarefas paralelas.
Por fim, faça uma auditoria de todos os webhooks de entrada. Verifique se eles têm idempotency key ou eventId, se você não tenta fazer trabalho pesado de forma síncrona no handler HTTP e se consegue sobreviver a uma onda de retries caso seu downstream caia temporariamente. Se não — mova a lógica para uma fila e para workers em background.
Essa sequência de passos dá um ganho bem significativo de resiliência mesmo sem infra super complexa.
8. Erros comuns ao trabalhar com timeouts, circuit breakers, bulkheads e tempestades de webhooks
Erro nº 1: ausência de timeouts “em algum ponto lá embaixo”.
Desenvolvedores costumam definir timeout apenas no Gateway ou apenas no frontend, esquecendo que, dentro do backend, ainda há BD, APIs externas e LLM. No fim, a requisição externa tem, digamos, timeout de 5 segundos, mas uma chamada ao BD ou ao sistema de pagamentos pode ficar pendurada por minutos, bloqueando o pool de conexões e provocando falhas em cascata.
Erro nº 2: timeouts gigantes “por via das dúvidas”.
Às vezes se coloca timeout de 60–120 segundos: “deixa rodar até o fim”. No contexto do ChatGPT, isso quase sempre é ruim. O usuário vai embora, o modelo começa a alucinar e seus recursos ficam bloqueados o tempo todo. É muito melhor uma falha honesta em 5–10 segundos com uma explicação clara.
Erro nº 3: circuit breaker sem UX bem pensado.
Às vezes o breaker é adicionado “pro-forma”, mas, quando dispara, o usuário ou o modelo recebe um 500 incompreensível, “ECONNREFUSED” ou “axios error”. O GPT, então, não consegue explicar adequadamente o que está acontecendo e começa a inventar. Vale pensar desde o início em mensagens de erro que façam sentido para pessoas e para o modelo.
Erro nº 4: mistura de recursos sem abordagem de bulkhead.
Cenário clássico: um serviço de recomendações (ou analytics) começa a ficar lento, consome todo o pool de conexões do BD ou o thread-pool e, em seguida, morrem o checkout e o login. Tudo porque os recursos não foram separados. A ausência de qualquer abordagem de bulkhead leva a que uma feature secundária possa derrubar todo o ambiente de produção.
Erro nº 5: tratar webhooks como requisições comuns.
Iniciantes frequentemente escrevem um handler de webhook como um controller comum: lógica de negócio longa, chamadas a APIs de terceiros, sem idempotência. Em condições de retries e duplicatas, isso leva a processamento duplo de eventos, estados estranhos de pedidos e quedas por carga durante uma tempestade.
Erro nº 6: ignorar idempotência em cenários de commerce.
É especialmente perigoso quando o webhook de pagamento pode criar um pedido novamente ou mudar seu estado em duplicidade. Sem verificar a idempotency key e sem armazenar o status de processamento do evento, cedo ou tarde você terá cobrança em duplicidade ou pedidos duplicados.
Erro nº 7: tentar consertar tudo com setTimeout e “atrasos mágicos”.
Às vezes tenta-se contornar race conditions e problemas de tempestade com “esperar 100 ms e vai dar certo”. Na prática, isso torna o comportamento ainda mais instável e não protege contra falhas reais. O caminho certo — timeouts explícitos, circuit breaker, filas e idempotência, e não “mágica” de atrasos.
Erro nº 8: falta de priorização de caminhos críticos.
Quando checkout e login vivem com os mesmos limites da analytics ou da lógica de recomendação, qualquer sobrecarga pode derrubar igualmente tanto partes críticas quanto secundárias. Em um design resiliente, checkout e auth são “vacas sagradas”: recursos separados, limites separados, alertas e SLO próprios.
GO TO FULL VERSION