CodeGym /Cursos /ChatGPT Apps /Gateway e proteção de perímetro: proxy, rate limiting, fi...

Gateway e proteção de perímetro: proxy, rate limiting, filas e backpressure

ChatGPT Apps
Nível 16 , Lição 1
Disponível

1. Por que proteger o perímetro do ChatGPT App

Em um app web clássico, o usuário — o navegador — acessa seus endpoints de forma relativamente previsível. No mundo dos ChatGPT Apps surge um novo tipo de cliente: uma LLM que decide sozinha quando e quais ferramentas chamar.

O modelo pode:

  • em uma única conversa chamar várias vezes a mesma tool;
  • experimentar: “e se chamar suggest_gifts de novo com parâmetros um pouco diferentes?”;
  • trabalhar em paralelo para centenas de usuários.

Some a isso possíveis bots, scripts de teste, erros no seu próprio código (por exemplo, um loop infinito que aciona continuamente uma tool‑call) e você terá quase a receita perfeita de um DoS de boa-fé.

A cereja do bolo é o custo. Cada tool‑call pode:

  • consumir APIs externas pagas (entregas, pagamentos, catálogos);
  • invocar outras LLM (por exemplo, busca RAG);
  • disparar tarefas pesadas em background.

Sem limites e proteção de perímetro, um único cliente “infeliz” pode:

  • derrubar todos os serviços de backend atrás do gateway (Gift API, Commerce API etc.);
  • estourar os limites de APIs externas;
  • e queimar significativamente o orçamento com modelos.

O objetivo desta aula é mostrar como gateway/proxy + rate limiting + filas + backpressure transformam essa potencial catástrofe em um sistema controlável.

Insight

A plataforma ChatGPT não fornece absolutamente nenhum mecanismo de proteção do seu servidor MCP contra tráfego externo. Qualquer cliente da internet pode enviar requisições para ele, incluindo utilitários como MCP Jam.

Tudo o que o ChatGPT pode oferecer é limitar o tráfego de entrada por endereços IP, configurando um reverse proxy (por exemplo, NGINX) para trabalhar com uma allowlist. Se a filtragem por IP não estiver configurada, seu servidor MCP fica totalmente exposto, o que é inseguro — nem para você nem para seus usuários.

2. Proxy/Gateway como “escudo” na frente dos serviços de backend e agentes

Primeiro, vamos relembrar o diagrama, agora pela ótica da proteção.

Imagine um esquema típico:

flowchart LR
  ChatGPT["ChatGPT / Widget"]
    --> GW["MCP Gateway (Auth, Rate Limit, Logs)"]

  GW --> GiftAPI["Gift REST API (seleção de presentes)"]
  GW --> CommerceAPI["Commerce REST API (checkout, ACP)"]
  GW --> Analytics["Analytics Service / REST API"]

  GW --> Queue["Fila de tarefas"]
  Queue --> Worker["Background workers"]

O Gateway fica entre o mundo externo (ChatGPT, webhooks, clientes de teste) e todo o resto. Ele:

  • vê absolutamente todas as requisições de entrada;
  • verifica primeiro o token e o formato da requisição;
  • é capaz de descartar coisas claramente impossíveis (host suspeito, path estranho, body grande demais);
  • decide para qual serviço interno REST/HTTP faz sentido encaminhar a requisição.

Nesse mesmo nível aparecem:

  • rate limiting — limitamos quantas requisições podem ser feitas por intervalo de tempo;
  • backpressure básico — recusamos se os serviços por baixo já estiverem engasgando;
  • mudança para assíncrono — coisas pesadas vão direto para a fila, resposta para o cliente: “aceito, aguardando”.

Ou seja, o gateway não é só um “router”, é também um “colete à prova de balas”. O principal é não transformá-lo no “monólito de todo o negócio”, como já falamos na aula passada.

3. Quais fluxos de tráfego precisam ser controlados

No ecossistema de um ChatGPT App normalmente há três principais tipos de tráfego que nos interessam do ponto de vista de limites e proteção.

Primeiro, MCP tool‑calls vindas do ChatGPT. É tudo que chega pelo protocolo MCP: chamadas a suggest_gifts, get_product_details, create_checkout_session e outras ferramentas. O modelo pode gerá-las de forma bastante intensa, especialmente quando há Agents por baixo.

Segundo, as requisições de saída dos nossos backends para APIs externas. Dentro do serviço, podemos ter nossos próprios limites para sistemas de terceiros: catálogos, logística, pagamentos. Violá-los pode gerar bloqueios, penalidades ou degradação de qualidade.

Terceiro, webhooks de entrada — notificações do ACP, provedores de pagamento (Stripe etc.), entregadores. Elas chegam independentemente da atividade dos usuários. Se nosso endpoint ficar lento ou responder com erro, o sistema externo começará a fazer tentativas de novo (retries) e pode causar uma “tempestade” de notificações repetidas.

Para o GiftGenius isso fica assim:

  • o usuário e o modelo chamam intensamente suggest_gifts e find_similar_gifts;
  • a tool de checkout aciona o ACP/backend de comércio;
  • após o pagamento, o provedor envia webhooks payment.succeeded / payment.failed.

Todos esses fluxos convergem para um ponto — o Gateway, então é justamente ali que faz sentido colocar “contadores, filtros e barreiras”.

4. Rate limiting: proteção básica e economia de dinheiro

O que é rate limiting no nosso contexto

Rate limiting é um mecanismo que limita a quantidade de requisições de um cliente específico por unidade de tempo. A ideia é tão antiga quanto a internet, mas no contexto de ChatGPT Apps ela resolve de cara três problemas:

  • impede que um único cliente (ou bug) derrube seus serviços;
  • ajuda a cumprir os limites de APIs externas;
  • protege seu bolso de chamadas de modelo fora de controle.

Algoritmos clássicos:

  • janela fixa (Fixed Window),
  • janela deslizante (Sliding Window),
  • balde de tokens (Token Bucket),

o que importa é entender conceitualmente: “em um minuto, no máximo N requisições”, “cada requisição consome um token, tokens são repostos à taxa X por segundo” etc. A implementação costuma ficar por conta de uma biblioteca ou do API Gateway.

Onde aplicar os limites

Os limites podem ser aplicados em níveis diferentes.

No nível de reverse proxy (Nginx, Cloudflare, AWS API Gateway) é conveniente:

  • cortar o tráfego mais selvagem por IP;
  • limitar o tamanho do corpo da requisição;
  • proteger contra padrões simples de DDoS.

No nível do MCP Gateway (aplicação) é útil fazer um rate limiting mais “inteligente”:

  • por usuário (userId do token);
  • por organização (tenantId);
  • por tipo de operação (por exemplo, limitar fortemente create_checkout_session e ser mais brando com search);
  • por fonte (webhook vs tool‑call).

E também é possível adicionar limites dentro de microserviços para operações especialmente caras, mas isso é um próximo nível de detalhamento.

Como escolher a chave para os limites

O erro mais comum é limitar por endereço IP. No caso do ChatGPT isso é quase inútil:

  • todas as requisições podem vir de um único range da OpenAI;
  • usuários diferentes “vão aparecer” atrás do mesmo IP.

O que nos interessa muito mais:

  • userId — o usuário específico no seu app;
  • tenantId — a organização (se você trabalha com B2B e um chat é usado por vários funcionários);
  • token de API ou clientId, se você tiver várias integrações.

No GiftGenius normalmente basta usar userId + tenantId, extraídos do token que o ChatGPT envia nas chamadas MCP.

Implementação simples de rate limiting em TypeScript

Suponha que temos um pequeno MCP Gateway em Express. Vamos adicionar um rate limiting simples: no máximo 30 tool‑calls por minuto por usuário.

// Rate limiting primitivo: N requisições por minuto por userId
const WINDOW_MS = 60_000;
const MAX = 30;
const hits = new Map<string, { ts: number; count: number }>();

function rateLimit(req: Request, res: Response, next: NextFunction) {
  const userId = (req.headers["x-user-id"] as string) ?? "anonymous";
  const now = Date.now();
  const rec = hits.get(userId) ?? { ts: now, count: 0 };

  if (now - rec.ts > WINDOW_MS) {      // A janela "expirou" — começamos de novo
    rec.ts = now;
    rec.count = 0;
  }
  rec.count += 1;
  hits.set(userId, rec);

  if (rec.count > MAX) {
    return res.status(429).json({
      error: "rate_limit_exceeded",
      retryAfterSec: 60,
      message: "Too many tool calls, please retry later."
    });
  }
  next();
}

Agora vamos usá-lo na rota do MCP:

// Aplicamos o middleware a todos os MCP tool-calls
app.post("/mcp/tools/call", rateLimit, async (req, res) => {
  const result = await callBackendForTool(req.body); // Chamada REST para as APIs Gift/Commerce/Analytics
  res.json(result);
});

Pontos-chave:

  • retornamos um erro significativo (error: "rate_limit_exceeded"), e não apenas 500;
  • o modelo consegue ler esse erro, entender o que aconteceu e explicar corretamente ao usuário, em vez de começar a alucinar.

Em produção real, os contadores, claro, não vivem na memória de um único processo, e sim no Redis ou outro armazenamento compartilhado, para funcionar em cluster. Mas para entender o princípio isso basta.

Rate limiting e limites no nível do gateway nos protegem da avalanche de requisições, mas não resolvem outro problema — algumas operações continuam muito pesadas e demoradas. Aqui, HTTP síncrono não é suficiente, e entram em cena filas e tarefas assíncronas.

5. Filas e tarefas assíncronas: quando o síncrono já não dá

O problema dos timeouts do ChatGPT

Mesmo que você ajuste com cuidado o rate limiting, o ChatGPT (e clientes HTTP em geral) não gosta quando a resposta demora demais. A plataforma limita o tempo de execução de uma tool‑call e, se você ficar esperando um “super‑algoritmo de recomendação” terminar, então:

  • o usuário verá um spinner eterno;
  • a plataforma encerrará a requisição por timeout;
  • o modelo vai concluir que “algo deu errado” e começar a inventar explicações.

Solução: mover operações pesadas para o modo assíncrono. Padrão clássico:

  1. O Gateway recebe a requisição.
  2. Coloca a tarefa em uma fila.
  3. Retorna imediatamente 202 Accepted com um jobId.
  4. Um worker separado lê tarefas da fila e processa.
  5. O cliente (nosso widget ou até o ChatGPT via uma tool extra) consulta periodicamente o status pelo jobId ou recebe uma notificação via evento MCP.

Em termos de ChatGPT App, isso costuma aparecer como duas ferramentas: a primeira tool recebe a requisição, coloca a tarefa na fila e retorna o jobId; a segunda permite ao modelo ou ao widget saber o status por esse jobId e buscar o resultado. Opcionalmente, o mesmo progresso pode ser espelhado por notificações MCP.

Mini‑fila para o GiftGenius (exemplo de código)

Suponha que temos uma ferramenta pesada generate_large_gift_report, que pode levar dezenas de segundos. Em um App real, ela retornaria apenas o jobId, e uma tool separada, get_report_status, permitiria ao modelo ou ao widget consultar o estado e buscar o resultado por esse jobId. No nível do Gateway, faremos um endpoint separado com fila para ela.

type Job = { id: string; payload: any };
const queue: Job[] = [];
const MAX_QUEUE = 100;

app.post("/mcp/tools/generate_report", (req, res) => {
  if (queue.length >= MAX_QUEUE) {
    return res.status(503).json({
      error: "system_busy",
      message: "System is busy, please retry later."
    });
  }

  const job: Job = { id: crypto.randomUUID(), payload: req.body };
  queue.push(job);
  res.status(202).json({ jobId: job.id, status: "accepted" });
});

E um worker simples, que a cada 200 ms pega uma tarefa:

async function processJob(job: Job) {
  // Aqui chamamos o serviço de backend real ou o workflow de agentes via REST
  await handleHeavyGiftReport(job.payload);
}

setInterval(async () => {
  const job = queue.shift();
  if (!job) return;
  await processJob(job);
}, 200);

É claro que este é um exemplo bem simplificado:

  • na vida real a fila vive no Redis, SQS, Kafka etc.;
  • o status da tarefa é armazenado em outro lugar, para ser consultado;
  • normalmente há vários workers.

Mas o conceito fica claro: o Gateway não mantém a requisição aberta até tudo terminar. Ele aceita, coloca em processamento e responde rapidamente.

6. Backpressure: como não se afogar na própria fila

Em que o backpressure difere do rate limiting

O rate limiting responde primeiro à pergunta: “quantas requisições um cliente pode fazer por intervalo de tempo?”. É proteção contra “um usuário ativo demais” ou um bug do cliente específico.

Já o backpressure diz: “quantas tarefas/requisições no total nosso sistema consegue digerir simultaneamente sem desmoronar?”. É sobre o volume geral de carga, independentemente da origem.

Exemplo:

  • rate limiting: “o usuário não pode chamar suggest_gifts mais de 30 vezes por minuto”;
  • backpressure: “a fila não pode ter mais de 100 tarefas pendentes, caso contrário começamos a recusar novas requisições”.

O ideal é que esses mecanismos se complementem: o rate limit mantém os clientes sob controle, o backpressure salva o sistema se muita gente chegar ao mesmo tempo.

Implementação simples de limitação de chamadas ativas

Uma das variantes mais simples de backpressure é limitar o número de chamadas ativas por baixo. Por exemplo: não manter mais de 50 tool‑calls ativas simultaneamente para um serviço de backend/REST específico (Gift API, Commerce API etc.).

let activeCalls = 0;
const MAX_ACTIVE = 50;

app.post("/mcp/tools/call", async (req, res) => {
    if (activeCalls >= MAX_ACTIVE) {
        return res.status(429).json({
            error: "gateway_overloaded",
            message: "Gateway is temporarily overloaded, please retry later."
        });
    }

    activeCalls += 1;
    try {
        const result = await callBackendForTool(req.body); // Chamada REST para as APIs Gift/Commerce/Analytics
        res.json(result);
    } catch (err) {
        console.error("Tool call error", err);
        res.status(500).json({ error: "internal_error" });
    } finally {
        activeCalls -= 1;
    }
});

O que acontece aqui:

  • enquanto a quantidade de requisições simultâneas for menor que MAX_ACTIVE, liberamos a nova chamada;
  • se o limite for atingido, respondemos imediatamente com um erro significativo;
  • é importante diminuir o contador no finally, para não perder “slots” em caso de erro.

Isso é o backpressure mais simples: falamos honestamente ao cliente: “não posso agora, tente mais tarde”, em vez de aceitar tudo sem pensar e morrer.

No futuro, você pode:

  • definir MAX_ACTIVE diferentes para tipos distintos de operação (por exemplo, quase sempre liberar checkout e limitar geração de relatórios de forma mais rígida);
  • alternar limites dinamicamente conforme as métricas de carga.

7. Webhooks e “tempestades”: protegendo eventos de entrada

Até agora olhamos principalmente para requisições iniciadas por nós ou pelo ChatGPT (tool‑calls, requisições de saída, async jobs). Mas há outra fonte importante de carga no Gateway — webhooks de entrada de sistemas externos.

Webhooks são o outro lado da moeda: se as tool‑calls são iniciadas por nós (via modelo), os webhooks são iniciados por um serviço externo. É justamente aquele terceiro tipo de tráfego da seção 4, que não controlamos no tempo e frequência, mas precisamos conseguir processar sem quedas. Provedor de pagamento, ACP, logística — todos enviam notificações (webhooks) para nosso endpoint a cada mudança importante: “pagamento aprovado”, “pedido criado”, “entrega atualizou o status”.

Os problemas começam quando:

  • nosso endpoint responde lentamente;
  • responde com erro;
  • fica indisponível periodicamente.

Então o serviço externo, seguindo boas práticas, começa a fazer tentativas de novo (retries). E, se você der azar, vai receber uma “tempestade” de webhooks — dezenas ou centenas de eventos repetidos tentando “alcançar” você a qualquer custo.

Para não morrer desse “cuidado”, no nível do Gateway vale a pena:

  1. Limitar webhooks de entrada por fonte: por exemplo, “no máximo 10 eventos por minuto por event_type de um provedor específico”.
  2. Validar a assinatura antes de fazer o parsing do JSON: assinatura HMAC ou mecanismo similar permite descartar requisições falsas.
  3. Tornar o processamento de eventos idempotente: por event_id ou campo equivalente, para que eventos repetidos não gerem pedidos ou pagamentos duplicados.
  4. Em tempestades fortes, ativar backpressure adicional: responder temporariamente “503: tente mais tarde” se os serviços downstream não estiverem acompanhando.

Exemplo simples (ideia, não código de produção):

app.post("/webhooks/stripe", rateLimitWebhook, (req, res) => {
    const sig = req.headers["stripe-signature"] as string;
    if (!isValidSignature(req.rawBody, sig)) {
        return res.status(400).send("Invalid signature");
    }

    const event = JSON.parse(req.body.toString());
    if (isAlreadyProcessed(event.id)) {
        return res.json({ received: true }); // idempotência
    }

    handleStripeEvent(event);
    res.json({ received: true });
});

Aqui, no nível do Gateway, nós:

  • aplicamos uma política de rate limiting separada para webhooks;
  • validamos a assinatura antes de confiar no conteúdo;
  • nos protegemos de duplicidades por meio de isAlreadyProcessed.

8. Aplicando ao GiftGenius: exemplo de política de limites e filas

Agora, saindo da abstração, vamos ver como isso pode ficar no nosso GiftGenius didático.

Considere três cenários-chave:

  1. Busca de presentes (suggest_gifts, find_similar_gifts).
  2. Criação de pedido / checkout (create_checkout_session, confirm_order).
  3. Recebimento de webhooks do provedor de pagamentos e do ACP.

Para cada cenário, faz sentido definir:

  • qual chave usamos para contar o limite;
  • quantas requisições por minuto são permitidas;
  • o que fazer ao ultrapassar.

Por exemplo:

Cenário Chave do limite Limite por minuto Comportamento ao exceder
Busca de presentes userId 30 429 + dica: “restrinja os parâmetros de busca”
Criação de pedido userId + tenantId 5 429 + texto: “tentativas demais, verifique os pedidos”
Webhooks de entrada provider + eventType 10 429/503, log, possível degradação

Para webhooks, normalmente é mais lógico limitar pela combinação “provedor + tipo de evento”, e cortar duplicidades com um mecanismo separado de idempotência por event_id.

No código, isso vira middlewares diferentes: rateLimitSearch, rateLimitCheckout, rateLimitWebhook.

Para operações pesadas, como “gerar um relatório PDF grande de presentes do ano”, usamos a fila e o padrão assíncrono que mostramos acima. Nesse caso, o Gateway:

  • recebe a requisição do ChatGPT;
  • coloca a tarefa na fila;
  • retorna um jobId e uma dica para o modelo sobre como obter o status;
  • limita o tamanho da fila (backpressure), para não saturar o sistema.

É importante lembrar: rate limiting e backpressure não são apenas sobre segurança e confiabilidade, mas também sobre UX. É muito melhor ouvir do assistente: “O serviço está sobrecarregado agora, vamos tentar em um minuto?”, do que ficar olhando um spinner até dar timeout ou ver um “Internal Server Error”.

9. Mini‑prático: adicionando proteção ao nosso MCP Gateway

Para não ficar só na teoria, vamos montar um mini‑prático que você pode implementar no seu projeto de estudo.

Rate limiting para todos os MCP tool‑calls

Adicione o middleware rateLimit (como acima) e conecte-o a /mcp/tools/call. Para começar, você pode usar um limite bem simples: 30 requisições por minuto por userId. Depois, faça alguns testes:

  • diminua o limite e veja como seu App e o modelo reagem;
  • faça limites diferentes para tipos distintos de tools, passando, por exemplo, toolName no middleware.

Backpressure mais simples por chamadas ativas

Adicione o contador activeCalls e o limite MAX_ACTIVE. Tente simular carga (por exemplo, com um script que envia um lote de requisições) e veja em que momento o Gateway começa a responder com gateway_overloaded.

O importante aqui é o comportamento: você não espera tudo cair; recusa novas tarefas, dizendo honestamente ao cliente que agora está muito carregado.

Fila para a ferramenta pesada

Escolha uma operação pesada (ou torne-a artificialmente “pesada” — inserindo um setTimeout/um fetch demorado) e migre para o padrão “fila + jobId”. No mínimo:

  • endpoint POST /mcp/tools/generate_report — coloca a tarefa na fila e retorna um jobId;
  • endpoint GET /jobs/:id — retorna o status (pending, done, error e, possivelmente, o resultado);
  • worker que, a cada X milissegundos, chama processJob.

Isso é suficiente para entender como ficará a integração real com BullMQ ou outro engine de fila.

10. Erros comuns na proteção do perímetro

Erro nº 1: Limitar apenas por IP.
No mundo dos ChatGPT Apps isso é quase inútil: a maior parte das requisições vem de endereços da OpenAI, e todos os seus usuários ficarão atrás do mesmo IP. No fim, alguém vai estourar o limite de todos, e o verdadeiro causador ficará desconhecido. O correto é limitar por userId, tenantId ou token, e usar IP apenas como filtro bem grosseiro no nível do reverse proxy.

Erro nº 2: Retornar um 500 “nu” em vez de um erro significativo.
Se ao exceder o limite ou em caso de sobrecarga você apenas envia 500 Internal Server Error, o modelo não entende nada e começa a inventar. Já um erro estruturado com código (rate_limit_exceeded, gateway_overloaded) e uma descrição legível permite que a LLM explique corretamente a situação ao usuário e, se necessário, tente novamente mais tarde.

Erro nº 3: Fazer uma fila infinita sem backpressure.
Às vezes parece tentador: “vamos apenas colocar tudo na fila e depois resolvemos”. Na prática, a fila cresce para milhares de tarefas, as latências aumentam, a memória acaba e os usuários não veem resultado. Sempre limite o tamanho da fila e a quantidade de operações ativas. É melhor recusar novas requisições com 503 ou 429 do que transformar a fila em um buraco negro.

Erro nº 4: Confiar apenas em rate limiting e ignorar webhooks.
Muitos protegem apenas o tráfego de entrada do ChatGPT e deixam os webhooks “como der”. Quando o provedor de pagamento começa a fazer tentativas de novo, são justamente os webhooks que podem criar uma tempestade. Endpoints de webhook precisam de limites próprios, verificação de assinatura e processamento idempotente. Caso contrário, é fácil ter uma dezena de duplicidades do mesmo pedido.

Erro nº 5: Manter todos os contadores e a fila apenas na memória de uma instância.
Para um projeto de estudo, tudo bem, mas em produção, ao escalar o Gateway para várias instâncias, os contadores em cada nó começarão a “viver a própria vida”, os limites deixarão de ser globais e reiniciar um nó zerará a fila. Em sistemas reais, para armazenar o estado de limites e filas usa-se um armazenamento comum (Redis, filas em nuvem etc.). Falaremos disso nas aulas sobre escalabilidade e produção.

Erro nº 6: Enfiar lógica de negócio dentro do Gateway “já que ele é intermediário de tudo”.
Surge às vezes a tentação: “vamos decidir no Gateway quais presentes mostrar, afinal as requisições passam por ali mesmo”. No fim, o gateway vira um monólito cheio de lógica, que é ao mesmo tempo roteador, cérebro de negócio e logger. Isso complica muito o escalonamento e a manutenção. O Gateway deve permanecer como camada de rede/infra: autenticação, autorização, limites, cache, roteamento — sim; seleção de presentes — não.

Erro nº 7: Achar que “somos pequenos, isso não nos afeta”.
Muitos pensam: “Não temos um milhão de usuários, dá para ficar sem gateway/limites”. Na prática, até um único bug no código do cliente (ou no prompt, que faz o modelo chamar a tool em loop) pode causar um pequeno, porém bem local, apocalipse. Rate limiting básico e ao menos um backpressure primitivo não são luxo; são a escova de dentes da produção: use desde o começo, antes de doer.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION