Ao pensar em filas, é comum pensarmos em filas com um ciclo de vida muito longo, filas que existem enquanto a aplicação existir.
É comum pensarmos em filas como recursos estáticos que fazem parte dos requisitos de funcionamento da aplicação, nascendo quando a aplicação é implantada em produção pela primeira vez e somente deixando de existir somente quando a aplicação é desativada ou substituída.
Hoje vamos abordar filas que possuem um ciclo de vida diferente, um ciclo de vida absolutamente curto, filas que podem durar de poucos milissegundos e semanas, e eventualmente até meses ou anos.
Não adianta torcer o nariz. Muitos aplicativos que estão no teu celular hoje fazem uso desse recurso em seus backends.
Desde 2013 usando e falando de RabbitMQ, ainda percebo certo incômodo quando falamos de filas não tradicionais.
Antes de avançarmos no ponto central do texto, preciso pontuar os tipos de filas, para entendermos exatamente do que estamos falando.
Tipos de Filas
No RabbitMQ temos 3 tipos de filas:
- Classic Queues
- Quorum Queues
- Streams*
Stream não é exatamente um tipo de fila, mas é apresentada no RabbitMQ como se fosse.
Seria um preciosismo desnecessário isolar streams, mas é importante entender que não são filas.
Classic queues
Classic queues são o tipo tradicional do RabbitMQ, compatíveis com todo o ecossistema (exchanges, DLX, TTL, plugins) e otimizadas para latência baixa e compatibilidade com clientes .NET (RabbitMQ.Client). Elas suportam filas duráveis e mirrors (federation/HA policies), mas o modelo de espelhamento clássico pode causar reordenação e penalidades em failover; são ideais para workloads gerais, baixa complexidade operacional e onde throughput moderado e integração imediata com features existentes são prioritários.
Quorum queues
Quorum queues são filas replicadas no cluster baseadas em Raft projetadas para alta disponibilidade e consistência forte, substituindo as mirrored queues para cargas críticas: tolerância a falhas mais previsível, menor risco de perda de mensagens e comportamento de eleição determinístico. Elas sacrificam um pouco de latência/throughput em comparação às classic queues em prol de durabilidade e segurança de dados — recomendadas para mensagens de missão crítica, billing, comandos financeiros ou onde a ordem e a persistência são requisitos firmes; funcionam bem com clientes .NET e suportam DLX/TTL, mas dimensionamento e tuning são necessários.
Streams
RabbitMQ Streams oferece um modelo baseado em log de alta performance e retenção longa, pensado para grandes volumes, leitura sequencial e replay (sem a limitação de consumo único por fila), aproximando-se de sistemas estilo Kafka. Streams garantem alto throughput, ordenação de partição e offset-based consumption, sendo ideais quando você precisa de retenção histórica, replay/consumer-groups e processamento de eventos em larga escala; possuem cliente diferenciado, mas podem usar a mesma estrutura de consumo AMQP existente e demandam arquitetura e operações diferentes (storage/retention tuning), por isso são a escolha certa quando ordenação, replay e escala são primordiais.
RabbitMQ Streams é uma estrutura de dados replicada persistente que pode realizar as mesmas tarefas que as filas: elas armazenam em buffer mensagens de produtores que são lidas por consumidores. No entanto, os fluxos diferem das filas em dois aspectos importantes:
- Como as mensagens são armazenadas
- Como as mensagens são consumidas.
Filas de Longa duração – O ciclo de vida padrão
O ciclo de vida mais comum adotado para filas no RabbitMQ é o de longa duração, que consiste em filas que existem durante toda a vida da aplicação.
Esse é o tipo de fila mais usado, mais comum, e é o que você precisa conhecer primeiro.
Os demais ciclos de vida são secundários.
Esse tipo de ciclo de vida das filas é típico em sistemas de todos os tipos, não há demérito nem perdas em sua utilização. Ele é o mais usado, pois atende aos mais vastos casos de uso, além de ser o ciclo de vida mais simples, mais fácil de ser implementado e o primeiro a ser ensinado.
Filas efêmeras
Filas de longa duração são ótimas para a maioria dos workloads. Entretanto, uma fila com múltiplos consumidores não permite determinar/forçar o envio para um consumidor específico. Não temos a capacidade de determinar para qual consumidor uma mensagem deve ser entregue.
E há um número substancial de casos de uso em que se faz necessário fazer esse roteamento avançado.
IoT
Em um cenário de IoT o dispositivo geralmente envia mensagens para uma fila de longa duração, entretanto, para receber mensagens, o dispositivo precisa ter uma fila própria.
Essa fila pode durar enquanto o dispositivo existir, sobrevivendo aos restarts do dispositivo, ou pode durar apenas enquanto o dispositivo estiver conectado ao RabbitMQ.
Ambas as abordagens são válidas e possuem prós e contras.
RPC
Em um fluxo RPC (Request / Response) que use filas, temos a demanda de criação de uma fila a cada request. Quem recebe esse request, em geral, são filas de longa duração, mas o Response é entregue em uma fila que foi criada dinamicamente e essa fila dura apenas alguns milissegundos, poucos segundos ou poucos minutos.
Para implementar RPC você precisa seguir regras estritas e rígidas, aqui exemplifico o server dessa relação:
- Uma fila de longa duração é previamente criada.
- Um ou múltiplos consumidores começam a consumir essa fila para atender os requests que chegarem.
- Esses consumidores são implementados de forma que, além do body, também farão a leitura dos cabeçalhos AMQP da mensagem em busca do cabeçalho ReplyTo. Esse cabeçalho informa para o consumidor, qual o endereço AMQP para envio do Response.
- Cada Request recebido, o consumidor processará a mensagem e, ao final, enviará o Response para o endereço AMQP contido na propriedade ReplyTo do Request.
Para esse fluxo ganhar vida o client dessa relação tem obrigações também:
- Antes de enviar o Request, o client deve solicitar ao RabbitMQ uma fila anônima que será responsável por receber o Response.
- O RabbitMQ deve retornar uma fila com um nome.
- Antes de publicar a mensagem na fila do server, o cliente deve adicionar um cabeçalho ReplyTo na mensagem, informando qual o endereço AMQP da fila de resposta.
- O cliente precisa parar a thread em que está em execução e aguardar o Response na fila anônima, consumindo essa fila.
- Após um timeout ou a chegada da resposta, a fila anônima deve ser apagada, e o processamento que estava parado deve continuar com a resposta recebida.
Eu já abordei RPC aqui em “RPC sob AMQP seduz enquanto mata… sua implantação de mensageria”
RPC sob AMQP seduz enquanto mata… sua implantação de mensageria
RPC vs Two Way Async
A obrigatoriedade de aguardar a resposta para prosseguir com o processamento, bloqueando a thread, é característico do fluxo RPC, tornando-o semelhante a um Request HTTP, só que obviamente sob AMQP, usando filas.
O motivo para um Request HTTP não ser resiliente, independente de termos ou não a capacidade de retentativa, se dá por conta de não conseguirmos tolerar um downtime significativo. Em um fluxo que faça um Request HTTP temos um estado de memória, em uma thread, e em caso de falha nesse processo, temos algum tempo para recuperar a operação, mas esse tempo não é indefinido.
Uma desconexão de um usuário, um crash do processo, uma falha de rede, qualquer coisa que faça a thread morrer, leva consigo aquele request. Uma queda de luz, um simples deploy, uma reorganização de instâncias da aplicação são tarefas capazes de tombar a thread que originou o Request que estava em andamento, fazendo-o se perder.
Aliás, um dos meios de entregar resiliência para esse
fluxo consiste em englobá-lo em um consumo de fila.
O ponto é que o fluxo RPC não é resiliente porque precisa do Response na mesma thread que fez o Request, e eventualmente essa thread pode não existir mais, perdendo o estado anterior e talvez sequer recebendo a resposta.
Da mesma forma que um Request HTTP não é resiliente, por conta da necessidade de aguardar uma resposta síncrona, o fluxo de RPC AMQP também não é resiliente pelo mesmo motivo.
Então, sempre que possível, evite RPC.
Em muitos cenários não precisamos realmente aguardar essa resposta na mesma thread que originou o Request. Podemos, inclusive, quebrar o fluxo em duas etapas:
- na primeira, o processo termina com o envio do pedido
- na segunda, ele é retomado apenas quando a resposta chega.
Nesse modelo, em vez de um fluxo Request/Response, temos dois Requests independentes, que se conectam de forma assíncrona.
Esse é um fluxo resiliente!
Esse fluxo é baseado em 2 filas de longa duração, o que o torna mais fácil, mais eficiente e de quebra: Resiliente.
É importante evitar a adoção de RPC, porque fluxos RPC não são resilientes.
RPC só ganha resiliência, com a adoção de um garantidor que possa englobar a operação toda, como a adoção de filas.
Dê preferência para Two Way Async, com filas de request de longa duração.
Chat Server
Imagine que você tem um servidor de chat, são múltiplas instâncias do servidor. Cada cliente se conecta a uma instância do seu servidor via WebSocket. Isso produz o cenário onde as pessoas que participam de uma sala ou grupo podem estar conectadas em instâncias diferentes. Dado o volume de usuários e o limite de conexões WebSocket seu servidor escala dinamicamente criando mais instâncias do server.
O problema é que muitas vezes múltiplas instâncias precisam receber a mesma mensagem, é aqui que uma fila só não atende, pois uma fila não tem a capacidade de entregar a mesma mensagem para múltiplos consumidores.
Ao mesmo tempo, há a necessidade de assegurar que as mensagens só serão encaminhadas para instâncias que possuem usuários interessados, caso contrário será necessário publicar tudo para todas as instâncias, o que gera um flood desnecessário e ineficiente.
Nesse cenário, temos uma fila por instância do server.
Na inicialização de uma nova instância do server, sua fila de trabalho é criada e quando a instância morre, sua fila morre junto.
Não se preocupe com perda de mensagens, há tratamento adequado, porém chato, que usa desativação de binding, dead letters etc para evitar que a exclusão cause perda de mensagns.
A regra de ouro
Toda vez que uma mensagem precisa ser endereçada:
- A um consumidor específico, entre muitos.
- Ou vários consumidores simultaneamente.
Temos a necessidade de uma topologia diferente do padrão, e começamos a pensar em filas por consumidor.
Entendendo Filas por Consumidor
Fila por consumidor é uma topologia onde cada consumidor presente em um determinado fluxo possui sua própria fila de trabalho, não compartilhando essa fila com outros consumidores.
Ao lidar com esse tipo de fluxo, precisamos considerar algumas coisas:
- O consumidor (e sua fila) pode não estar mais disponível no momento em que enviamos uma mensagem para ele. O par de consumidor e fila podem ter morrido.
- A morte do consumidor pode acontecer enquanto sua fila de trabalho ainda não foi inteiramente consumida.
Assim, temos de nos preocupar tanto com o roteamento para filas que não existem mais, quanto como lidar com as mensagens não processadas quando uma fila é deletada.
Esses dois novos cenários se apresentam diante desse novo fluxo e temos mecanismos sofisticados presentes na plataforma para lidar com isso.
Alternate Exchange
As exchanges possuem um atribuido “alternate-exchange”. É uma forma elegante e robusta de lidar com mensagens não roteáveis no momento da publicação.
Em vez de deixar a mensagem ser descartada quando nenhum binding corresponde ao routing key, o broker encaminha essa mensagem automaticamente para uma exchange alternativa (“fallback”) definida como argumento da exchange original. Isso oferece uma trajetória determinística para captura, auditoria ou tratamento compensatório sem depender do flag mandatory do publisher ou de lógica de client-side.
Como funciona
- É um argumento da exchange: “alternate-exchange” => “<nome-da-exchange>” no momento da declaração de uma exchange.
- Aciona-se quando uma mensagem chega a uma exchange e nenhum queue binding a essa exchange corresponde ao routing key.
- O broker repassa a mensagem para a alternate exchange preservando headers, propriedades e routing key (ou seja, a mensagem chega intacta ao fallback).
- A alternate exchange processa a mensagem como uma publicação normal: geralmente fanout, e pode ter suas próprias filas, DLX, TTL etc.
- Se a alternate exchange também não conseguir roteá-la (e não tiver outra AE), a mensagem será descartada — portanto é importante projetar a AE para garantir captura (por exemplo, um fanout para uma fila de auditoria).
Diferenças importantes vs outras estratégias
- vs mandatory + basic.return: mandatory instrui o broker a retornar a mensagem ao publisher quando não roteável; isso exige que o publisher trate o retorno e mantenha conexão síncrona. Alternate exchange remove essa obrigatoriedade do publisher e delega o fallback ao broker.
- vs dead-letter exchange (DLX): DLX trata mensagens que já entraram em uma fila e foram rejeitadas, expiraram ou atingiram max-length. Alternate exchange atua no momento da publicação, antes mesmo de qualquer fila existir.
- vs publisher confirms: confirms garantem que o broker recebeu a mensagem; não dizem nada sobre roteabilidade — AE lida especificamente com mensagens não roteáveis.
Com Alternate Exchange conseguimos fazer com que, caso um consumidor não exista, a mensagem possa ser encaminhada para uma fila de controle e possa ser reprocessada por outro mecanismo, ou ainda possa seguir outro fluxo.
Dead Letter
Dead-lettering é o mecanismo padrão para capturar mensagens que não puderam ser processadas normalmente dentro de uma fila — seja por rejeição explícita, expiração ou limites de tamanho ou até PELA EXCLUSÃO DA FILA. O padrão permite redirecionar essas mensagens para uma exchange de tratamento (dead-letter exchange, DLX) e, tipicamente, para uma dead-letter queue (DLQ) onde serão inspeccionadas, reprocessadas ou descartadas de forma controlada.
Como funciona
- Configuração: você declara uma fila com argumentos: x-dead-letter-exchange = “<nome-dlx>” e opcionalmente x-dead-letter-routing-key = “<rk>”.
- Gatilhos de dead-letter:
- consumer chama basic.reject/basic.nack com requeue=false;
- mensagem expira por TTL (x-message-ttl em fila ou header expiration);
- fila atinge max-length/max-length-bytes (mensagens que excederem são dead-lettered).
- fila é deletada
- Quando dead-lettered, o broker publica a mensagem na DLX como uma nova publicação. RabbitMQ acrescenta/atualiza o header x-death (contendo contagem, motivo, queue original, exchange original, timestamp) para rastrear histórico de rejeições.
Conectando os pontos
Essas abordagens fazem uso não somente das operações QueueDeclareAsync e QueueBindAsync, mas também fazem uso de parâmetros pouco discutidos das filas. Esses casos de uso nos fazem começar a pensar nas flags:
- durable: Essa fila deve sobreviver à reinicialização do broker?
- exclusive: O uso desta fila deve ser limitado à conexão declarada? Essa fila será excluída quando a conexão declarada for encerrada.
- autoDelete: Essa fila deve ser excluída automaticamente quando seu último consumidor (se houver) cancelar a assinatura?
Esses parâmetros começam a ganhar vida e fazer sentido com esses tipos de caso de uso e abordagem.
De um lado, eliminando esforço manual de manutenção (exclusão) de filas quando elas não são mais úteis. Por outro asseguram que o consumo físico por múltiplos consumidores de diferentes conexões não será permitido, eliminando a necessidade de controle adicional.
Quem deve criar filas, exchanges e binds?
É super comum encontrar times discutindo de quem é a responsabilidade de criar filas, exchanges e binds, ou seja, a topologia de objetos do RabbitMQ.
É por conta dessa segunda camada de complexidade, quando nos deparamos com casos de uso mais complexos, que não há uma decisão que seja universalmente correta.
A depender das estratégias, temos demandas diferentes que precisam de abordagens diferentes.
A topologia no RabbitMQ é decisão de arquitetura, a depender da estratégia, essa criação pode, sim, ser uma tarefa de Infra ou Ops, ou DevOps, mas há casos de uso em que claramente é o código da aplicação quem tem de fazê-lo. E, em alguns casos, por operação, como descrito no case de RPC.
Assim, é importante entender que o correto a ser adotado é aquilo que traga a menor carga cognitiva, menor demanda por operação e manutenção e esforço diário.
Sem entender quais padrões serão adotados, não é possível determinar de quem é a responsabilidade, portanto, qualquer tentativa de atribuir um responsável, sem que saibamos quais padrões usaremos, seria inútil.
A distância entre o overview e o mundo real é muito grande, e por isso é importante aprofundar no entendimento dos padrões ao redor de Mensageria em especial do RabbitMQ, pois geralmente muitas das respostas simplesmente aparecem ao entender o comportamento.
Acredito que fazer provas de conceito seja uma forma muito eficiente de estudar, mesmo que você não tenha demanda para esse tipo de topologia.
Nos últimos 3 anos me deparei pela primeira vez com demandas que endereçaram um tipo específico de topologia que havia estudado há quase 10 anos e que ficaram engavetados esse tempo todo.
Somente conhecendo, mesmo que não implementando de fato, foi possível adotá-los anos mais tarde, mas porque o conceito estava devidamente absorvido, e muitas vezes isso demanda tempo.

Acima mostro algumas configurações necessárias para lidar com ordenação determinística em filas efêmeras. Essa quantidade de flags é uma demonstração de que quando saímos do overview, o básico feijão com arroz já não atende mais. E para conseguir entregar esses cenários avançados, é preciso ir mais fundo.








0 comentários