AAAbel Aguiar
← Voltar para o blog
backendfilasmensageriaarquitetura

Filas e mensageria no backend: quando usar, como modelar e onde errar menos

Como usar filas para proteger a experiência do usuário, desacoplar processamento e operar tarefas assíncronas com mais previsibilidade

Abel Aguiar·
Filas e mensageria no backend: quando usar, como modelar e onde errar menos

Fila costuma entrar no projeto depois que a equipe já sentiu o custo de fazer tudo dentro da requisição HTTP. No começo, parece natural validar dados, salvar no banco, chamar uma integração externa, enviar email e gerar arquivo antes de responder. Enquanto o volume é baixo e as dependências colaboram, funciona.

O problema aparece quando o cliente clica em "finalizar compra" e a API fica esperando o provedor de nota fiscal, o gateway de pagamento, o disparo de email e a atualização de analytics. A experiência do usuário passa a depender de sistemas que não estão sob o mesmo controle.

Fila não serve para deixar diagrama de arquitetura mais sofisticado. Serve para proteger o fluxo principal, desacoplar trabalho lento e permitir que o backend absorva variações de carga sem transformar toda oscilação externa em lentidão visível.

A primeira pergunta é de produto

Antes de escolher RabbitMQ, SQS, Redis ou qualquer outro broker, eu começaria com uma pergunta de produto: o usuário precisa mesmo esperar por essa tarefa?

Em um checkout, salvar o pedido e confirmar a intenção de compra costuma ser parte do caminho síncrono. Enviar email de confirmação, gerar PDF de recibo, sincronizar CRM e atualizar um relatório analítico normalmente podem acontecer logo depois, fora da resposta principal.

Esse recorte importa porque fila não remove trabalho. Ela muda quando e como o trabalho acontece. O usuário recebe uma resposta mais rápida, mas o sistema passa a precisar acompanhar o que ficou pendente e mostrar estado coerente quando alguém consulta depois.

O fluxo fica simples no desenho e mais exigente na operação

O desenho inicial é fácil de entender: a aplicação publica uma mensagem, a fila segura essa mensagem e um worker processa quando puder. RabbitMQ apresenta esse modelo com work queues, publish/subscribe e roteamento de forma bem didática.

Na prática, essa mudança altera a natureza da falha. Sem fila, o erro explode na frente do usuário. Com fila, o erro pode acontecer dois minutos depois, fora do request original. Isso exige registro de status, correlation ID, política de retry e uma forma de saber se a operação realmente terminou.

Um fluxo de geração de relatório ilustra bem:

  1. O usuário solicita o relatório.
  2. A API cria um registro com status pendente.
  3. Um job é enviado para a fila com o reportId.
  4. O worker gera o arquivo e marca concluido ou falhou.
  5. A tela consulta o status e mostra o resultado quando estiver pronto.

Fila bem usada não esconde o trabalho. Ela explicita o estado dele.

Mensagem pequena costuma envelhecer melhor

Um erro comum é transformar a mensagem em réplica do banco. Em vez de publicar tudo o que o worker precisa hoje, normalmente é melhor publicar o identificador do recurso, o tipo de trabalho e metadados de correlação.

{
  "type": "invoice.issue",
  "invoiceId": "nf-9482",
  "requestedAt": "2026-05-12T14:10:00Z",
  "correlationId": "checkout-7fd1"
}

Com isso, o worker busca o estado mais recente no banco. Se a regra muda, você corrige a origem da verdade em um lugar. Se o payload inteiro estiver congelado na mensagem, fica mais fácil processar informação desatualizada.

Existem cenários orientados a evento em que carregar mais contexto faz sentido. Mas como padrão inicial, mensagens enxutas são mais simples de versionar e investigar.

Idempotência separa fila útil de fila perigosa

Todo consumidor deve assumir que uma mensagem pode chegar mais de uma vez. Em sistemas como SQS, por exemplo, a própria documentação deixa claro que o modelo de entrega pode repetir mensagens; o visibility timeout evita processamento simultâneo no caso comum, mas não elimina a possibilidade de duplicidade.

Isso significa que o worker de cobrança não pode cobrar duas vezes porque recebeu o mesmo job de novo. O worker de email não deveria disparar cinco confirmações por causa de uma falha transitória. A lógica precisa reconhecer a operação já processada.

Na prática, isso pode significar:

  1. Guardar uma chave única da operação.
  2. Registrar status no banco antes e depois da execução.
  3. Validar se a transição de estado ainda faz sentido.
  4. Usar constraints para impedir duplicidade quando possível.

Se processar duas vezes causa prejuízo, o problema não é o broker. É o contrato do job.

Retry sem critério só desloca o incidente

Retentar é útil quando a falha é temporária. Uma integração que ficou indisponível por 30 segundos pode funcionar na segunda tentativa. Um CPF malformado não vai se corrigir com retry exponencial.

Por isso, vale separar erro transitório de erro definitivo. BullMQ suporta retries com backoff, e o mesmo raciocínio aparece em brokers e serviços gerenciados de outras stacks: o número de tentativas e o intervalo entre elas precisam refletir o tipo de falha.

Quando a mensagem falha de forma persistente, ela precisa sair do fluxo normal. É o papel da DLQ. A documentação do Amazon SQS recomenda usar dead-letter queues para isolar mensagens não processadas, investigar a causa e, quando fizer sentido, fazer redrive depois da correção.

DLQ não é cemitério. Se ninguém monitora, ela vira apenas um estoque silencioso de problema.

Ack, timeout e processamento longo precisam combinar

Outro erro comum é tratar confirmação de consumo como detalhe. Em brokers como RabbitMQ, acknowledgements manuais e publisher confirms existem justamente para melhorar segurança de entrega e processamento.

Se o worker reconhece a mensagem antes de terminar e cai no meio do trabalho, você pode perder processamento. Se nunca reconhece, o broker pode redeliver indefinidamente e acumular mensagens pendentes. O equilíbrio depende do tipo de fila e do broker, mas a regra geral é simples: confirme quando o efeito desejado já aconteceu ou ficou garantido.

No SQS, o visibility timeout precisa acompanhar o tempo real do job. Se o processamento leva dois minutos e a invisibilidade dura 30 segundos, outra instância pode receber a mesma mensagem antes da primeira terminar. A AWS recomenda ajustar ou estender esse timeout conforme a duração da tarefa.

Fila não elimina raciocínio sobre concorrência. Ela obriga o time a torná-lo explícito.

Ordem raramente é tão garantida quanto parece

Muita gente imagina fila como uma linha perfeita. Na prática, ordem global costuma ser uma promessa perigosa. RabbitMQ documenta que múltiplos consumidores, redelivery e prioridades afetam a ordem observada no processamento.

Se você tem eventos pedido_criado, pedido_pago e pedido_cancelado, não é prudente assumir que qualquer worker sempre receberá tudo na ordem perfeita sem proteção adicional. Em alguns cenários, particionar por chave, usar message group ou validar o estado atual antes de aplicar uma transição é indispensável.

O desenho mais confiável costuma ser aquele em que o worker pergunta: "essa mudança ainda é válida para o estado atual?". Não apenas: "recebi essa mensagem, então vou executar".

Observabilidade de fila é parte do produto

Fila sem painel operacional vira escuro organizado. Quando alguém pergunta "por que meu relatório não saiu?", não basta dizer "está na fila". O time precisa saber se a fila está fluindo, travada ou falhando.

Eu acompanharia pelo menos:

  1. Backlog pendente.
  2. Idade da mensagem mais antiga.
  3. Taxa de consumo.
  4. Tempo médio e p95 de processamento.
  5. Taxa de falha por tipo de job.
  6. Volume em DLQ.

Esses números mostram coisas diferentes. Backlog crescente sugere capacidade insuficiente. Mensagem antiga aponta degradação de SLA. DLQ crescendo mostra erro que retry não resolveu. A própria documentação do RabbitMQ destaca métricas de comprimento de fila, estados das mensagens e throughput para operação.

Onde o desenho costuma se perder

Fila melhora arquitetura quando vem acompanhada de contrato e operação. Sem isso, ela apenas esconde lentidão atrás de processamento assíncrono.

Os tropeços mais frequentes são previsíveis: job sem timeout, mensagem grande demais, retry agressivo, falta de idempotência, worker sem log útil, DLQ sem dono e ausência de métrica para backlog. Nenhum deles aparece no slide de arquitetura. Todos aparecem em incidente.

Um caminho pragmático

Se eu estivesse introduzindo fila em um backend hoje, começaria por poucos fluxos bem escolhidos:

  1. Uma tarefa claramente fora do caminho crítico, como email ou relatório.
  2. Uma integração externa sujeita a instabilidade.
  3. Um processamento pesado que já atrasa request.

Para cada fluxo, eu definiria mensagem pequena, status persistido, correlation ID, regra de retry, DLQ, idempotência e um painel básico. Só depois ampliaria o uso para processos mais centrais.

Não precisa começar com uma arquitetura dirigida por eventos para toda a empresa. Em muitos produtos, uma fila bem modelada já resolve o problema concreto com menos cerimônia.

Conclusão

Filas e mensageria ajudam o backend a suportar trabalho assíncrono com mais previsibilidade. Elas encurtam requests, amortecem dependências instáveis e criam espaço para processar no ritmo correto.

Mas esse ganho vem com responsabilidade. O sistema precisa lidar com duplicidade, falha tardia, retry, DLQ, timeout, ordem e observabilidade. Quando essas peças entram no desenho desde o começo, fila deixa de ser remendo de performance e passa a ser um componente confiável da arquitetura.

O resultado é um backend que responde melhor ao usuário e oferece mais controle ao time quando algo inevitavelmente sai do caminho feliz.

Posts relacionados