CodeGym /Cursos /ChatGPT Apps /Confiabilidade dos fluxos: rate limits, backpressure e mo...

Confiabilidade dos fluxos: rate limits, backpressure e monitoramento de eventos

ChatGPT Apps
Nível 13 , Lição 4
Disponível

1. Por que os fluxos são especialmente sensíveis à carga

Nas partes anteriores, já analisamos como são estruturados os eventos MCP, os status job.progress/job.completed, as jobs assíncronas e os canais de streaming (SSE/HTTP-stream) para o GiftGenius. Agora é importante ver o que acontece com essa arquitetura sob carga real.

Enquanto você tem um único usuário que raramente inicia a busca de presentes, tudo parece ótimo. Mas assim que o GiftGenius vai para produção e passam a chegar simultaneamente centenas de solicitações para “escolher presentes para todos os funcionários na festa da empresa”, você de repente descobre que:

  • no servidor há centenas de conexões SSE de longa duração;
  • os workers enviam job.progress a cada espirro;
  • os logs crescem em gigabytes por dia;
  • a UI do usuário começa a engasgar, embora “o servidor aparentemente não esteja caindo”.

Uma requisição HTTP clássica vive milissegundos ou segundos. Um fluxo SSE ou HTTP‑stream pode viver minutos e até horas. Ele mantém a conexão, memória, descritores de arquivo. Cada evento enviado é serialização JSON, cópia pela rede, trabalho do GC. Se você tratar isso como “ah, é só mais um console.log no back‑end”, o sistema rapidamente vira um aquecedor.

Os eventos MCP têm outra característica: eles muitas vezes são gerados múltiplas vezes para a mesma tarefa. Um worker que atualiza o progresso a cada 0,1% gera um número impressionante de eventos por job. No fim, você obtém “ruído”: uma quantidade enorme de mensagens pequenas que:

  • sobrecarregam a rede e a CPU;
  • entopem filas e buffers;
  • tornam a depuração e a análise de logs dolorosas.

Por isso, tanto os fluxos quanto os eventos MCP devem ser tratados como chamadas ao banco de dados ou chamadas de modelo: são recursos caros que exigem normatização, controle e monitoramento.

Para lidar com isso, mantenha três grandes temas em mente:

  1. Rate limits — limitar quantos e com que frequência podemos gerar e enviar eventos/fluxos.
  2. Backpressure — reagir quando o consumidor não acompanha o produtor.
  3. Monitoramento e métricas — medir o que acontece e perceber a tempo quando tudo começa a ferver.

2. Rate‑limiting de fluxos e eventos

Vamos começar pelo mais óbvio — limites.

É importante entender que, em cenários de streaming, quem muitas vezes faz o papel de “infrator perigoso” não é o cliente, mas o servidor. Em APIs REST comuns, você limita o número de requisições ao servidor para que o usuário não vire um DDoS. No mundo do MCP e dos fluxos é muito fácil causar um DDoS invertido: o worker ou o servidor MCP bombardeia o cliente com milhares de eventos por segundo.

Quais limites são necessários

Normalmente, pensamos em três dimensões.

Em primeiro lugar, limites por usuário ou sessão. Não é possível permitir que um usuário abra vinte assistentes do GiftGenius em paralelo, cada um com seu fluxo SSE. Um limite razoável: alguns fluxos ativos por sessão e limitar o número de jobs em status running para um usuário ou locatário.

Em segundo lugar, limites por job. Aqui nos interessa a frequência dos eventos. Basta enviar job.progress no máximo uma vez a cada N milissegundos ou apenas quando houver uma alteração perceptível, por exemplo a cada 5% de progresso. Não é necessário enviar uma mensagem para cada produto processado no catálogo. Também faz sentido limitar o tamanho do payload: um evento de progresso não deve carregar megabytes de texto.

Em terceiro lugar, limites por IP ou organização. Isso já é proteção contra abusos, quando alguém executa um script que envia tarefas em massa, ou quando seu App fica inesperadamente popular. Aqui entram em jogo mecanismos conhecidos de API gateways e proxies.

Implementação simples de limite de frequência de eventos

Vamos considerar um worker do GiftGenius que, em segundo plano, seleciona presentes para uma longa lista de destinatários e periodicamente envia o progresso via MCP notification event/progress. Queremos que os eventos sejam enviados no máximo uma vez a cada 500 milissegundos e apenas quando o percentual mudar pelo menos 5 pontos.

Pseudo‑código TS condicional para o worker:

// suponha que exista algum mcpClient.sendNotification(...)
let lastSentPercent = 0;
let lastSentAt = 0;

function reportProgress(jobId: string, percent: number, message: string) {
  const now = Date.now();
  const percentDelta = percent - lastSentPercent;
  const timeDelta = now - lastSentAt;

  // enviar apenas se passaram >= 500 ms OU aumentou >= 5%
  if (percentDelta >= 5 || timeDelta >= 500) {
    mcpClient.sendNotification("event/progress", {
      jobId,
      percent,
      message,
    });
    lastSentPercent = percent;
    lastSentAt = now;
  }
}

Essa abordagem se chama throttling: nós “afinamos” o fluxo de eventos no tempo e pela variação do valor.

Se você divide em etapas (“Etapa 1 de 3”, “Etapa 2 de 3”), a lógica é ainda mais simples: enviar eventos apenas quando o estágio mudar.

Limite para a quantidade de fluxos abertos simultaneamente

No lado do servidor MCP você provavelmente tem um handler HTTP de SSE:

// app/api/events/[userId]/route.ts (Next.js 16 App Router)
export async function GET(
  req: Request,
  { params }: { params: { userId: string } },
) {
  const userId = params.userId;

  if (!canOpenMoreStreams(userId)) {
    return new Response("Too many streams", { status: 429 });
  }

  const stream = new ReadableStream({
    start(controller) {
      registerSseClient(userId, controller);
    },
    cancel() {
      unregisterSseClient(userId);
    },
  });

  return new Response(stream, {
    headers: { "Content-Type": "text/event-stream" },
  });
}

A função canOpenMoreStreams pode verificar o número atual de conexões abertas para o usuário e comparar com o limite (por exemplo, no máximo três fluxos paralelos). Se o limite for excedido, retornamos 429 e, nas instruções do GPT, explicamos ao modelo que, nessa situação, é melhor não iniciar outro assistente demorado, mas sugerir ao usuário que “já há uma busca ativa; vamos aguardar o término dela”.

Em sistemas pequenos, tais verificações podem ser implementadas na memória do processo. Em infraestruturas mais sérias, isso vai para um MCP gateway ou um serviço dedicado de rate limit.

3. Backpressure: o que fazer quando o consumidor não acompanha

Rate limits limitam quanto queremos produzir de eventos. Mas mesmo com limites cuidadosos, ainda é possível que o consumidor “se engasgue”: o usuário tem internet móvel ruim, a aba do navegador travou, o ChatGPT está muito carregado naquele momento.

Backpressure é a reação do sistema ao fato de que o consumidor não acompanha. Em vez de acumular dados infinitamente e, cedo ou tarde, cair com OOM, nós conscientemente:

  • diminuímos o ritmo;
  • agregamos eventos;
  • descartamos os menos importantes.

Onde a pressão surge

Um cenário típico para o GiftGenius pode ser assim: o worker grava eventos em uma fila (por exemplo, Redis Streams ou apenas uma tabela no BD), o servidor MCP os lê e envia pelo canal SSE. Se o cliente for lento (3G, notebook antigo, muitas outras abas), o buffer TCP começa a encher, o processo Node não consegue esvaziar totalmente a fila e acaba acumulando eventos na memória. Depois você vê o conhecido:

FATAL ERROR: Ineffective mark-compacts near heap limit

Backpressure no nível de rede (TCP) você já tem, mas ele não conhece suas entidades de domínio. Ele apenas diz: “Ei, desacelera, o buffer está cheio”. Nossa tarefa é interpretar isso no nível dos eventos MCP.

Bufferização com limite e descarte de eventos

Para progresso e status, temos uma vantagem: nem todos os eventos têm o mesmo valor. Para o usuário, é importante saber o último percentual atualizado, e não o histórico de todos os intermediários “51%, 52%, 53%, 54%”. Isso significa que podemos descartar parte dos eventos e enviar apenas o mais recente.

Suponha que haja uma camada que recebe eventos de progresso dos workers e os coloca em um buffer para cada jobId:

type ProgressEvent = { jobId: string; percent: number; message: string };

const progressBuffers = new Map<string, ProgressEvent[]>();
const MAX_BUFFER = 10;

function bufferProgress(event: ProgressEvent) {
  const buffer = progressBuffers.get(event.jobId) ?? [];
  buffer.push(event);

  // limitar o tamanho do buffer
  if (buffer.length > MAX_BUFFER) {
    // manter apenas os últimos alguns eventos
    progressBuffers.set(event.jobId, buffer.slice(-MAX_BUFFER));
  } else {
    progressBuffers.set(event.jobId, buffer);
  }
}

Um timer separado, por exemplo a cada 500 ms, observa o buffer e envia apenas o último evento, ignorando os demais:

setInterval(() => {
  for (const [jobId, buffer] of progressBuffers.entries()) {
    if (!buffer.length) continue;

    const last = buffer[buffer.length - 1];
    sendProgressToClient(last); // SSE/MCP notification

    progressBuffers.set(jobId, []); // limpar
  }
}, 500);

Este é um exemplo da tática de conflation: combinar várias atualizações em uma única atual. Para progresso — é um padrão de ouro.

Para eventos do tipo “log” ou partial_result, a estratégia pode ser diferente. Lá, a perda de eventos muitas vezes é inaceitável: o texto dos logs é importante, e um JSON chunk perdido pode quebrar a estrutura dos dados. Nesses casos, você pode:

  • agregar a mensagem (juntar várias linhas de log em um único pacote);
  • ou enviar um sinal de controle ao worker “reduza a geração de logs”.

Em sistemas assíncronos, a segunda opção é mais difícil, mas vale ao menos considerá‑la.

Limitação da profundidade das filas

Backpressure não se limita ao buffer de eventos logo antes do envio. É preciso observar todas as filas no sistema:

  • a fila de tarefas aguardando um worker;
  • a fila de eventos entre o worker e o servidor MCP;
  • buffers dentro das bibliotecas de streaming no lado do servidor.

Para cada fila é importante definir um limite razoável de profundidade. Se a fila se enche, você ou começa a responder aos clientes “o sistema está sobrecarregado, tente novamente mais tarde”, ou descarta jobs menos importantes, ou converte parte dos cenários para “modo off‑line” (por exemplo, gera um relatório e envia um link depois).

Uma técnica interessante é a priorização de tipos de eventos. Sob sobrecarga, você pode começar a enviar apenas job.completed e job.failed, e job.progress reduzir de prioridade ou desligar completamente.

4. Monitoramento de fluxos e eventos

Sem medições, toda essa beleza com rate limits e backpressure vira feitiçaria. É preciso enxergar que os fluxos ficaram suspeitamente numerosos, que os eventos chegam com lag e que os clientes estão caindo em massa.

Os fluxos se comportam de forma diferente das requisições HTTP comuns: sua duração pode ser medida em minutos e horas, portanto métricas clássicas como “requisições por segundo” e “latência média” não dão o quadro completo.

Métricas‑chave

Para fluxos SSE ou HTTP/stream, é útil acompanhar alguns grupos de indicadores.

  1. Métricas de conexão. Quantos fluxos SSE estão ativos agora? Quanto tempo, em média, vive uma conexão? Qual é a porcentagem de fluxos que terminam com erro ou timeout? Um pico acentuado de conexões ativas indica uma possível tempestade de tráfego ou vazamento de recursos (clientes não fecham as conexões). Uma queda brusca indica um desligamento em massa (por exemplo, problemas de rede ou um bug crítico no servidor).
  2. Métricas de eventos. Quantos eventos você envia por segundo em todos os fluxos (EPS — events per second, basicamente o número de eventos por segundo)? Qual é o tamanho médio do evento? Quantos erros de desserialização ou de validação do payload você observa? Se você de repente perceber aumento do tamanho dos eventos, talvez alguém tenha começado a enviar em job.progress, em vez de uma string curta, todo o texto do relatório.
  3. Métricas de jobs. Distribuição por status (pending, running, completed, failed, canceled), tempo médio de execução por tipo de tarefa, porcentagem de jobs que vão para retry ou dead‑letter. Isso ajuda a entender que os problemas não estão apenas na camada de rede, mas também nos workers: uma API externa ficou mais lenta, surgiram erros em massa.
  4. Métricas de backpressure e indicadores do sistema. Em sistemas de streaming, costuma‑se observar a profundidade dos buffers e das filas entre componentes, bem como a porcentagem de tempo em que o fluxo fica bloqueado, aguardando o consumidor liberar espaço. Se suas filas estão quase sempre lotadas, é um sinal claro de que o sistema está no limite. Também é importante monitorar indicadores do sistema: CPU e memória nos servidores que fazem streaming e erros/timeouts no nível de rede. Às vezes, a própria largura de banda da rede entre o servidor MCP e o ChatGPT vira gargalo.

Somadas, essas quatro categorias respondem a três perguntas: quantos fluxos estão vivos agora, quanta informação você está trafegando, como os jobs se comportam e onde exatamente o sistema começa a se engasgar.

O que registrar em log

Logs — o segundo pilar da observabilidade. É importante registrar eventos e conexões de modo que depois seja possível reconstruir o histórico para um job específico.

Normalmente, você adiciona aos logs de cada evento e fluxo:

  • jobId e/ou eventId;
  • userId e sessionId (se houver multi‑tenant);
  • tipo do evento (progress, completed, failed, resource.updated);
  • tipo do canal (SSE ou HTTP/stream);
  • timestamp de envio e, se possível, timestamp de nascimento do evento no worker.

Assim é possível calcular o lag: a diferença entre o momento em que o worker gerou o evento e o momento em que ele saiu pelo socket. O aumento desse tempo de lag é um bom indicador de problemas com backpressure.

É preciso cuidado para que os próprios logs não virem fonte de sobrecarga. Para eventos de alta frequência como job.progress, registrar cada evento nem sempre é sensato; você pode ativar sampling — registrar cada N‑ésimo evento em vez de todos — ou agregar estatísticas.

Em código, isso pode parecer um helper simples:

function logEvent(event: {
  type: string;
  jobId: string;
  userId?: string;
  channel: "sse" | "http-stream";
  payload: unknown;
}) {
  console.info({
    ...event,
    timestamp: new Date().toISOString(),
  });
}

Em um projeto real, você envolve isso com uma biblioteca de logging estruturado, mas a ideia é a mesma: o máximo de contexto útil em cada entrada.

5. Alertas e políticas de degradação

Quando você já tem métricas e logs, o próximo passo é configurar alertas e pensar em como o sistema deve “degradar” quando estiver em apuros. A ideia é que é melhor funcionar honestamente pior do que cair de repente.

Exemplos de alertas

Para o GiftGenius, faz sentido acompanhar algumas situações típicas.

Em primeiro lugar, um número anormal de fluxos ativos. Se normalmente você tem dezenas de conexões SSE ativas e, de repente, surgem milhares, vale descobrir o que está acontecendo. Pode ser que você ficou popular, ou pode ser um bug e as conexões não estão sendo fechadas.

Em segundo lugar, a latência entre a conclusão efetiva da job e o recebimento de job.completed pelo cliente. Se essa latência começa a ultrapassar o limite (digamos, 510 segundos), significa que em algum ponto entre o worker e o cliente os eventos estão se acumulando ou as conexões estão patinando.

Em terceiro lugar, alta proporção de job.failed ou job.canceled em comparação às bem‑sucedidas. A causa pode estar no worker (API externa quebrada, novo bug) ou na maior sensibilidade do usuário a atrasos (eles começam a cancelar tarefas com mais frequência).

Por fim, nível elevado de erros de conexão e quebras de stream: se cresce o número de disconnects não normais, talvez haja problemas na rede ou no lado do cliente, e vale pensar em cenários de fallback.

Padrões de degradação

Quando o sistema está sobrecarregado, você pode ativar um “modo de economia de recursos”. Isso é melhor do que simplesmente começar a responder 500 para tudo.

O padrão mais comum é a frequência adaptativa de eventos. Se você perceber que o event‑rate (número de eventos por segundo) disparou dez vezes acima do normal e o lag nas filas começou a crescer, diminua a frequência dos eventos de progresso. Era a cada 1% — faça a cada 10%. Era a cada 500 ms — faça uma vez a cada 23 segundos. O usuário vive muito bem sem um progresso superpreciso; já com uma UI totalmente travada — nem tanto.

Para eventos menos importantes — por exemplo, resource.updated durante a atualização em segundo plano do feed de produtos — você pode desativar temporariamente o envio enquanto o sistema estiver sob carga.

Outro truque é migrar parte dos cenários de fluxos para polling periódico. Se os canais SSE caírem, o servidor MCP pode enviar ao widget um evento de sistema como system.overloaded, e o widget alternar para a estratégia “a cada N segundos consulto o endpoint REST sobre o status da job”.

6. Pequeno trecho prático para o GiftGenius

Para juntar tudo, vamos supor que já temos:

  • MCP tool startGiftSearch, que cria uma job e retorna jobId;
  • um worker que executa a busca e envia event/progress e event/completed;
  • endpoint SSE /api/events/[userId], ao qual o widget em Next.js se conecta.

Vamos adicionar uma camada simples de proteção contra “tempestade de eventos” e um monitoramento mínimo.

Limitação do progresso por passos e tempo

No worker, adicionamos throttling e conflation, como discutido acima. Agora os eventos são enviados no máximo uma vez a cada meio segundo e quando a alteração for de pelo menos 5%.

Controle de fluxos ativos

No endpoint SSE, mantemos um contador por usuário:

const activeStreams = new Map<string, number>();
const STREAM_LIMIT = 3;

function canOpenMoreStreams(userId: string) {
  const current = activeStreams.get(userId) ?? 0;
  return current < STREAM_LIMIT;
}

function registerSseClient(userId: string, controller: ReadableStreamDefaultController) {
  const current = activeStreams.get(userId) ?? 0;
  activeStreams.set(userId, current + 1);

  // aqui você salva o controller em alguma estrutura,
  // para depois escrever eventos nesse fluxo
}

function unregisterSseClient(userId: string) {
  const current = activeStreams.get(userId) ?? 1;
  activeStreams.set(userId, Math.max(0, current - 1));
}

As métricas de activeStreams.size o servidor pode enviar adicionalmente para Prometheus/Grafana ou qualquer outro sistema de monitoramento.

Métrica mais simples de event‑rate

Para começar, dá para ao menos contar quantos eventos enviamos:

let eventsSentLastMinute = 0;

function sendProgressToClient(ev: ProgressEvent) {
  // ... serialização e escrita no fluxo SSE
  eventsSentLastMinute++;
}

setInterval(() => {
  console.info({
    metric: "events_per_minute",
    value: eventsSentLastMinute,
    timestamp: new Date().toISOString(),
  });
  eventsSentLastMinute = 0;
}, 60_000);

Com o tempo, isso pode ser trocado por contadores completos e alertas, mas como ponto de partida — já é bem razoável.

Se juntarmos tudo acima: limites, backpressure, métricas/alertas e um UX de fallback adequado fazem com que seu GiftGenius deixe de ser “demo para demo” e suporte tempestades reais de tráfego. Nos próximos módulos, quando falarmos de gateway, arquitetura de produção e observabilidade de verdade, esses padrões ainda serão úteis.

7. Erros típicos ao trabalhar com fluxos, rate limits e monitoramento

Erro nº 1: ausência de limites para a quantidade de fluxos e a frequência de eventos.
Os desenvolvedores adicionaram SSE “para ficar bonito”, os workers enviam progresso para cada objeto processado, e tudo parece funcionar no demo. Mas, no primeiro pico de usuários reais, o servidor passa a gastar a maior parte dos recursos serializando e transmitindo milhares de eventos minúsculos, e a UI no ChatGPT vira um slide show.

Erro nº 2: tentar bufferizar “tudo de uma vez” sem limites.
Aparece no código um array ilimitado de “eventos não enviados”, que cresce até que o cliente se recupere. Spoiler: ele não se recupera, o servidor morre antes. Todo buffer deve ter um máximo rígido, e a lógica de tratamento do estouro precisa ser explícita.

Erro nº 3: tratar todos os tipos de evento da mesma forma.
Progresso pode ser agregado e descartado (o último percentual é mais importante do que o histórico do movimento). Com logs e resultados parciais isso não pode ser feito — a perda de um chunk pode significar dados corrompidos. Ao projetar o sistema, agrupe os eventos por importância com antecedência e defina, para cada grupo, uma estratégia sob sobrecarga.

Erro nº 4: falta de observabilidade.
Nenhuma métrica de fluxos ativos, nenhum controle do event‑rate, e nos logs — apenas “algo deu errado”. Nessa situação você só descobre os problemas pelos relatos de usuários e pelo gráfico de carga de CPU. Configurar ao menos métricas básicas e logs por jobId e eventId não é luxo, é necessidade.

Erro nº 5: UX rígido que não considera degradação.
O widget e as instruções do GPT partem da premissa de que o fluxo está sempre disponível, o progresso é atualizado “em tempo real” e os partial results chegam rigorosamente conforme o roteiro. Nos primeiros problemas de rede, o usuário vê uma barra de progresso “congelada” e nenhuma explicação. É muito melhor prever no UX um fallback honesto: “No momento há problemas com atualização em tempo real; vou continuar a busca e avisar quando terminar” — e passar para atualizações mais raras ou polling.

Erro nº 6: confiar que “nossos usuários não criarão muitas tarefas simultâneas”.
A prática mostra que, se você não limitou o número de jobs e fluxos paralelos, alguém inevitavelmente abrirá cinco abas, iniciará em cada uma uma busca de presentes “no máximo” e irá tomar um café. A ideia de “talvez a sorte ajude” em produção quase sempre termina com um encontro com o monitoramento sob o estrondo dos alertas.

1
Pesquisa/teste
Notificações, nível 13, lição 4
Indisponível
Notificações
Notificações e cenários de streaming (eventos MCP)
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION