Filas de mensagem: por que desacoplar processamento pesado da requisição

Entenda o problema de uma requisição HTTP travada esperando um processamento demorado, como uma fila de mensagem resolve isso de forma assíncrona, e um exemplo prático de produtor e consumidor com RabbitMQ.


Já falei aqui sobre webhook e sobre feature flag, dois jeitos de desacoplar partes de um sistema. Fila de mensagem resolve um desacoplamento parecido, mas voltado pra um problema específico: o que fazer quando uma requisição precisa disparar um processamento que demora demais pra terminar dentro do tempo normal de resposta de uma API.

O problema da requisição travada

Imagina um endpoint que, ao ser chamado, precisa gerar um relatório em PDF com milhares de linhas, ou mandar e-mail pra uma lista de dez mil usuários. Se esse processamento inteiro roda dentro da própria requisição HTTP, o usuário fica esperando, a conexão fica aberta o tempo todo, e se o processamento demorar mais que o timeout configurado (no navegador, no load balancer, ou no próprio servidor), a requisição simplesmente falha, mesmo que o processamento continuasse rodando por trás.

// ruim: a requisição só responde depois do trabalho pesado terminar
app.post("/relatorios", async (req, res) => {
  const pdf = await gerarRelatorioGigante(req.body);
  res.json({ pdf });
});

Como a fila resolve isso

Uma fila de mensagem separa dois papéis: quem pede o trabalho (o produtor) e quem executa o trabalho (o consumidor), e no meio dos dois fica a fila, seja RabbitMQ, SQS, ou outra ferramenta parecida, guardando as mensagens até que alguém esteja pronto pra processar cada uma.

Em vez do endpoint fazer o trabalho pesado direto, ele só publica uma mensagem na fila e responde na hora:

// produtor: só publica a mensagem, não faz o trabalho pesado
app.post("/relatorios", async (req, res) => {
  await fila.publicar("gerar_relatorio", { usuarioId: req.body.usuarioId });
  res.json({ status: "processando" });
});

Um processo separado, rodando de forma independente do servidor da API, fica escutando essa fila e processa cada mensagem no próprio ritmo:

// consumidor: roda separado, processa uma mensagem por vez
fila.consumir("gerar_relatorio", async (mensagem) => {
  const pdf = await gerarRelatorioGigante(mensagem.usuarioId);
  await salvarPdf(mensagem.usuarioId, pdf);
  await notificarUsuario(mensagem.usuarioId);
});

A resposta da API volta imediatamente, avisando que o processamento começou, e o usuário é notificado depois (por e-mail, por WebSocket, que já expliquei aqui, ou checando um status numa tela) quando o relatório realmente fica pronto.

O ganho além da resposta rápida

Separar produtor e consumidor traz outro benefício que passa despercebido no começo: os dois lados podem escalar de forma independente. Se a fila acumula mensagem mais rápido do que consegue processar, basta subir mais instâncias do consumidor, sem precisar tocar no servidor da API. E se o consumidor cair por qualquer motivo, a mensagem continua guardada na fila, esperando alguém processar, em vez de simplesmente se perder como aconteceria se o processamento estivesse preso dentro da própria requisição HTTP.

Reprocessamento e fila de erro

Quando um consumidor falha ao processar uma mensagem, o comportamento padrão da maioria das filas é devolver ela pra fila, pra tentar de novo depois. Isso é útil pra falha temporária (um serviço externo fora do ar por um instante), mas perigoso sem limite: uma mensagem com problema permanente (um dado inválido que sempre quebra o processamento) ficaria tentando pra sempre, num loop infinito.

A solução padrão é a dead letter queue, uma fila separada pra onde a mensagem vai depois de um número máximo de tentativas falhas:

fila.consumir("gerar_relatorio", async (mensagem) => {
  try {
    await gerarRelatorioGigante(mensagem.usuarioId);
  } catch (erro) {
    if (mensagem.tentativas >= 3) {
      await fila.publicar("relatorios_com_erro", mensagem);
    } else {
      throw erro; // devolve pra fila original tentar de novo
    }
  }
});

Isso evita que uma mensagem problemática trave o processamento das outras, e ainda mantém um registro claro do que falhou de verdade, pra alguém investigar depois, em vez de simplesmente sumir sem deixar rastro.

Quando vale a pena

Fila de mensagem não é pra todo tipo de operação. Uma requisição que já responde rápido, tipo buscar um usuário no banco, não ganha nada com essa complexidade extra. O ganho aparece em operações que demoram mais do que um tempo razoável de resposta HTTP, que podem ser processadas depois sem prejudicar a experiência de quem fez a requisição, ou que se beneficiam de escalar o processamento de forma independente da API.

Fechando

O ponto central é separar "receber o pedido" de "executar o trabalho", permitindo que a API responda rápido mesmo quando o trabalho de verdade demora. A fila é o que garante que esse pedido não se perde entre um lado e o outro, mesmo que o consumidor esteja ocupado, fora do ar por um instante, ou processando numa velocidade diferente da que as mensagens chegam.