Filas e mensageria: por que sistemas robustos conversam por mensagens
Filas desacoplam produtor de consumidor, absorvem picos e sobrevivem a quedas.
Neste artigo
Chame um serviço, espere a resposta, siga em frente — é assim que a maioria dos sistemas nasce, e por bom tempo funciona. O problema aparece quando um dos lados fica lento, cai, ou recebe mais trabalho do que consegue absorver de uma vez. Nesse momento, a chamada síncrona deixa de ser um detalhe de implementação e vira o motivo pelo qual o sistema inteiro trava. Filas e mensageria existem para resolver exatamente essa classe de problema: em vez de dois serviços conversarem diretamente e precisarem estar de pé ao mesmo tempo, um terceiro elemento — o broker — recebe a mensagem de um lado e entrega ao outro quando ele puder processá-la.
Este artigo explica o que é mensageria, por que ela desacopla sistemas de um jeito que chamadas diretas não conseguem, os padrões fundamentais de fila e publicação/assinatura, as garantias de entrega que todo consumidor precisa respeitar, e quando essa peça simplesmente não é a ferramenta certa.
O problema da comunicação síncrona#
Numa chamada síncrona — HTTP, gRPC, uma chamada de função remota qualquer — o serviço A pede algo ao serviço B e fica esperando a resposta antes de continuar. Essa espera parece inofensiva até você olhar para as consequências em cadeia.
Acoplamento temporal: os dois precisam estar de pé ao mesmo tempo. Se B está fora do ar quando A tenta chamá-lo, o pedido falha ali mesmo, não importa se B volta a funcionar um segundo depois. A dependência não é só lógica — é uma dependência de disponibilidade simultânea. Quanto mais serviços numa cadeia síncrona, maior a chance de pelo menos um estar indisponível no instante exato da chamada, e a confiabilidade do conjunto tende à confiabilidade do elo mais frágil, multiplicada pelo número de saltos.
Lentidão se propaga para trás. Se B está processando devagar — banco sob carga, fila interna cheia, um recurso externo lento — A fica esperando, com uma thread ou uma conexão presa até o timeout. Se A por sua vez é chamado por um serviço C, a lentidão sobe um nível. Numa arquitetura com várias chamadas síncronas em cadeia, um único componente lento no fim da linha consegue arrastar todo o caminho até o usuário, um efeito conhecido como falha em cascata: filas de espera se enchem, threads se esgotam, e serviços que não tinham nada de errado começam a rejeitar pedidos só porque estão presos esperando alguém mais adiante.
Picos de carga não têm para onde ir. Se dez mil pedidos chegam no mesmo minuto e o serviço que os processa aguenta mil por minuto, a chamada síncrona não tem alternativa: ela rejeita, atrasa ou derruba o serviço, porque não existe onde guardar o excedente. O produtor e o consumidor estão amarrados ao mesmo ritmo, e esse ritmo é ditado pelo mais lento dos dois.
O nome técnico para isso é acoplamento — não só de contrato (A precisa saber o formato que B espera), mas de tempo (A precisa que B esteja disponível agora) e de ritmo (A não pode produzir mais rápido do que B consome). Mensageria ataca as duas últimas formas de acoplamento diretamente.
A ideia central: um intermediário que guarda a mensagem#
A mudança de modelo é simples de enunciar e profunda em consequência: em vez de A chamar B diretamente, A publica uma mensagem num broker e segue em frente sem esperar. O broker guarda essa mensagem com segurança até que B — o consumidor — esteja pronto para pegá-la, processá-la e confirmar que terminou.
Desacoplamento no tempo. A não precisa que B esteja disponível no exato instante da publicação. B pode estar caído, reiniciando, fazendo deploy, ou simplesmente ocupado processando o que veio antes — a mensagem espera na fila até ele voltar. Isso transforma uma falha momentânea de B, que antes derrubaria a chamada de A, num atraso invisível que se resolve sozinho quando B volta.
Desacoplamento no espaço. A não precisa saber onde B está, quantas réplicas de B existem, nem sequer que B é quem vai processar a mensagem — A só sabe que publica num destino nomeado (uma fila, um tópico) e o broker cuida da entrega. Trocar a implementação de B, escalar B para dez instâncias, ou até substituir B por outro serviço inteiramente, não exige nenhuma mudança em A.
Desacoplamento de identidade. Numa chamada direta, A conhece o endereço de B. Numa fila, A publica uma mensagem — "pedido criado", "imagem enviada", "e-mail para disparar" — sem saber, e sem precisar saber, quem vai consumi-la. Isso é o que permite, no padrão de publicação/assinatura, que múltiplos consumidores completamente diferentes reajam ao mesmo evento sem que o produtor tenha sido escrito pensando em nenhum deles.
Um fluxo conceitual simples ilustra a mecânica: um serviço de checkout publica a mensagem pedido.criado com o payload do pedido; a fila armazena essa mensagem; um worker de processamento de pagamento a consome, cobra o cartão e confirma (ack) a mensagem, que só então desaparece da fila. Se o worker cair no meio do processamento, a mensagem nunca foi confirmada — ela volta a ficar visível e outro worker (ou o mesmo, depois de reiniciar) a pega de novo. O checkout, do seu lado, terminou a operação no instante em que publicou; ele nunca soube, nem precisou saber, que o worker caiu.
Os benefícios concretos#
Desacoplar não é um fim em si — é o que abre espaço para um conjunto de propriedades que sistemas puramente síncronos não conseguem ter.
Absorção de picos, ou nivelamento de carga. A fila funciona como um amortecedor entre uma taxa de chegada irregular e uma taxa de processamento estável. Dez mil pedidos podem entrar na fila em um minuto; o consumidor processa mil por minuto, no seu próprio ritmo, e os outros nove mil esperam a vez sem se perder. Sem a fila, esse mesmo pico teria que ser absorvido — ou rejeitado — na hora, geralmente derrubando o serviço. Com ela, um pico vira uma fila mais longa por alguns minutos, não uma indisponibilidade.
Resiliência a falhas do consumidor. Se o consumidor cai — crash, deploy, falta de recurso — as mensagens não se perdem: elas continuam na fila, esperando. Quando o consumidor volta, ele retoma exatamente de onde parou, processando o que se acumulou. Compare com uma chamada síncrona: se B cai no meio de um pedido de A, esse pedido específico se perde e alguém — geralmente o usuário, com um erro na tela — precisa lidar com isso.
Escala horizontal por competing consumers. Várias instâncias do mesmo consumidor podem puxar mensagens da mesma fila, cada mensagem processada por exatamente uma delas. Precisa de mais capacidade de processamento? Sobe mais instâncias do consumidor; elas competem pelas mensagens disponíveis e o throughput cresce quase linearmente, sem nenhuma mudança no produtor nem no formato da mensagem.
Isolamento de falhas entre componentes. Um consumidor lento ou instável não arrasta o produtor para baixo junto com ele, porque não há uma conexão aberta e esperando entre os dois. O produtor publica e segue; o problema fica contido no lado do consumidor, onde pode ser corrigido, escalado ou reiniciado sem afetar quem gerou a mensagem.
Dois padrões fundamentais: fila e publicação/assinatura#
"Mensageria" cobre duas formas de entrega com semânticas bem diferentes, e escolher a errada para o problema gera confusão previsível.
Fila (point-to-point): uma mensagem, um consumidor#
No modelo de fila clássico, cada mensagem publicada é entregue e processada por exatamente um consumidor, mesmo que existam vários consumidores competindo pela mesma fila. É o modelo certo para distribuição de trabalho: uma tarefa não deve ser executada duas vezes. Redimensionar uma imagem enviada pelo usuário, enviar um e-mail transacional, gerar um relatório em segundo plano — em todos esses casos, você quer que exatamente um worker pegue a tarefa e a complete, não que cinco workers redimensionem a mesma imagem cinco vezes.
Publicação/assinatura (pub/sub): uma mensagem, todos os interessados#
No modelo pub/sub, o produtor publica num tópico, e todo assinante daquele tópico recebe uma cópia da mensagem, independentemente de quantos assinantes existam. É o modelo certo para broadcast de eventos de domínio: quando "pedido criado" acontece, o serviço de estoque precisa reservar o item, o serviço de e-mail precisa notificar o cliente, e o time de dados precisa registrar o evento para analytics — três reações completamente independentes ao mesmo fato, nenhuma delas ciente da existência das outras. O produtor publica um único evento; cada assinante decide, por conta própria, se e como reage.
A confusão mais comum é tratar um tópico pub/sub como se fosse uma fila de trabalho, esperando que só um consumidor "vença" a mensagem — ou o oposto, esperando que uma fila de tarefas replique a mesma tarefa para vários workers. Os dois modelos resolvem problemas diferentes, e a escolha do padrão é, na prática, a escolha de qual pergunta você está fazendo: "quem processa isso uma vez?" ou "quem mais precisa saber que isso aconteceu?".
Fila de tarefas versus log de eventos#
Existe ainda uma terceira variação que vale distinguir conceitualmente, popularizada por sistemas no estilo Kafka: o log de eventos. Numa fila tradicional, a mensagem desaparece quando é confirmada — ela existiu para ser consumida uma vez. Num log de eventos, a mensagem é persistida numa sequência ordenada e continua lá depois de lida; cada consumidor mantém seu próprio ponteiro de leitura (o offset) dentro desse log, e pode reler, avançar ou até voltar no tempo, de forma independente dos outros consumidores. Isso combina a semântica de broadcast do pub/sub com a durabilidade e a capacidade de reprocessamento de um histórico completo — útil quando o valor não está só no evento em si, mas na sequência completa de eventos como fonte de verdade.
Garantias de entrega: o que "confiável" realmente significa#
Nenhum broker garante entrega perfeita de graça; toda garantia é uma escolha explícita, com uma consequência que o consumidor precisa saber lidar.
At-most-once: a mensagem pode se perder, nunca duplicar. O broker entrega e esquece; se o consumidor cair antes de processar, a mensagem já era. É aceitável só quando perder uma mensagem ocasional é tolerável — uma métrica de telemetria não-crítica, por exemplo — e nunca para algo como um pagamento ou um pedido.
At-least-once: a mensagem nunca se perde, mas pode chegar duplicada. É a garantia padrão da maioria dos brokers de fila, porque é a mais fácil de implementar com segurança: o broker só remove a mensagem depois de receber confirmação explícita, então qualquer falha de comunicação no meio do caminho — o consumidor processou mas a confirmação se perdeu, por exemplo — leva o broker a reentregar a mesma mensagem. Essa garantia exige, por construção, que todo consumidor seja idempotente: processar a mesma mensagem duas vezes precisa produzir o mesmo resultado que processá-la uma vez, seja checando um identificador único já visto antes de agir, seja usando uma operação naturalmente idempotente (SET saldo = 100 em vez de saldo += 10). Ignorar esse requisito é a causa mais comum de cobranças duplicadas e efeitos colaterais repetidos em sistemas baseados em fila.
Exactly-once: raro, caro, e geralmente uma ilusão parcial. Entregar exatamente uma vez, sem duplicar e sem perder, exige coordenação forte entre produtor, broker e consumidor, e mesmo quando um sistema anuncia essa garantia, ela costuma valer só dentro de fronteiras específicas (por exemplo, dentro do próprio ecossistema do broker, não fim a fim até um efeito externo como uma cobrança de cartão). Na prática, a engenharia mais robusta assume at-least-once e resolve a duplicação com idempotência no consumidor, em vez de depender de uma garantia exactly-once fim a fim.
Ack, nack e visibilidade: como a mensagem realmente desaparece#
O mecanismo por trás de "a mensagem espera até ser processada" tem um nome e uma lógica precisos. Quando um consumidor pega uma mensagem, ela não é removida da fila — ela fica marcada como invisível por um período (o visibility timeout), como se estivesse emprestada. Se o consumidor processa com sucesso e envia um ack (acknowledgment) dentro desse período, a mensagem é removida de vez. Se o consumidor trava, cai, ou explicitamente recusa a mensagem com um nack, ela volta a ficar visível na fila — ou imediatamente, ou depois de um atraso — para ser entregue de novo, a esse ou a outro consumidor.
Esse mecanismo é o que garante a resiliência descrita antes: um worker que morre no meio do processamento não perde a tarefa, porque a mensagem nunca foi confirmada. Mas ele também é a razão pela qual a idempotência importa tanto — o mesmo mecanismo que evita perda é o que garante reentrega, e reentrega é duplicação em potencial.
Dead Letter Queue: quando uma mensagem não pode ser processada#
Nem toda mensagem processa com sucesso na segunda tentativa, ou na décima. Um payload malformado, uma regra de negócio que nunca vai ser satisfeita, um bug que sempre lança exceção para aquele caso específico — sem um limite, essa mensagem voltaria para a fila, seria pega de novo, falharia de novo, indefinidamente. Esse cenário tem nome: poison message, uma mensagem que nunca vai passar e que, sem contenção, consome capacidade de processamento sem parar e pode travar o restante da fila atrás dela.
A Dead Letter Queue (DLQ) é a contenção: depois de um número configurado de tentativas falhas, o broker move a mensagem para uma fila separada, dedicada a mensagens problemáticas, em vez de devolvê-la à fila principal indefinidamente. A fila principal continua fluindo normalmente para todas as outras mensagens; a mensagem problemática fica isolada, disponível para inspeção manual, reprocessamento depois de corrigido o bug, ou descarte deliberado. Uma DLQ sem monitoramento é só um cemitério silencioso — o ponto de ter uma é olhar para ela e agir, não simplesmente parar de ver o erro.
Ordenação: nem sempre garantida, e isso custa caro#
É tentador presumir que mensagens são processadas na ordem em que foram publicadas, mas isso não é verdade por padrão na maioria dos sistemas de fila, especialmente quando há múltiplos consumidores competindo. Se a mensagem M1 falha e volta para reentrega enquanto M2, publicada depois, já foi processada com sucesso por outro consumidor, a ordem de conclusão não bate com a ordem de publicação.
Garantir ordem estrita tem um custo direto em paralelismo: se só um consumidor pode processar por vez, dentro de uma mesma chave lógica (por exemplo, todas as mensagens de um mesmo pedido), você abre mão de parte da escala horizontal que competing consumers oferece. A técnica comum é particionar por uma chave — todas as mensagens do mesmo pedido vão para a mesma partição, processadas em ordem dentro dela, enquanto partições diferentes processam em paralelo entre si. A pergunta a fazer antes de exigir ordenação global é se ela é realmente necessária para o domínio, ou se o que parece precisar de ordem estrita na verdade só precisa de ordem dentro de uma entidade, que é bem mais barato de garantir.
O padrão outbox: publicar um evento sem perdê-lo#
Existe uma armadilha sutil e comum: um serviço grava uma mudança no banco de dados e, na sequência, publica um evento correspondente na fila — duas operações contra dois sistemas diferentes, sem transação que as amarre. Se o processo cai entre as duas escritas, ou se a publicação falha silenciosamente, o banco reflete uma mudança que o resto do sistema nunca soube que aconteceu. Um pedido é criado, mas o evento pedido.criado nunca sai — o estoque nunca é reservado, o e-mail nunca é enviado, e nada no log denuncia isso até alguém notar a ausência.
O padrão outbox transacional resolve isso trocando "grava no banco e publica na fila" por "grava no banco e publica na mesma transação, mas contra uma tabela, não contra o broker". A escrita de domínio e a inserção na tabela de outbox acontecem atomicamente, na mesma transação — ou as duas acontecem, ou nenhuma. Um processo separado (um poller ou um leitor de log de replicação) lê essa tabela e publica de fato no broker, marcando cada linha como enviada depois da confirmação. Como a gravação do evento pendente é parte da mesma transação que o dado de negócio, não existe mais janela em que um foi salvo e o outro não — a garantia de atomicidade do banco cobre os dois.
Quando não usar fila#
Mensageria resolve um problema específico — desacoplar produtor de consumidor no tempo e no ritmo — e não é a ferramenta certa quando esse problema não existe.
Quando você precisa da resposta imediatamente. Se o chamador não pode continuar sem saber o resultado — validar uma senha no login, calcular um preço para mostrar na tela, checar se um item está disponível antes de confirmar a compra — introduzir uma fila só adiciona uma etapa assíncrona a um fluxo que é síncrono por natureza. Uma chamada request-response direta, com timeout e tratamento de erro adequados, é mais simples de entender, depurar e operar.
Quando a operação é rápida, síncrona e local por definição. Nem toda comunicação entre componentes precisa de desacoplamento — dentro de um mesmo serviço, uma chamada de função é a ferramenta certa, não um evento publicado e consumido. Mensageria compensa a complexidade que adiciona (mais um componente de infraestrutura para operar, latência assíncrona, necessidade de idempotência) quando o ganho em resiliência e desacoplamento supera esse custo. Quando não supera, é complexidade sem retorno.
Quando a simplicidade operacional pesa mais que o desacoplamento. Um broker de mensageria é mais um sistema para manter no ar, monitorar e entender sob falha. Para um sistema pequeno, com poucos componentes e baixa necessidade de escalar cada um de forma independente, a comunicação síncrona direta, bem feita — com timeout, retry com backoff e circuit breaker — cobre boa parte do que a mensageria ofereceria, com uma peça de infraestrutura a menos para operar.
Vale mencionar, sem entrar em resenha de produto, que a categoria de brokers de fila tradicionais (no estilo RabbitMQ, ou os serviços gerenciados de fila oferecidos por provedores de nuvem, como o SQS da AWS) atende bem o modelo de distribuição de trabalho e pub/sub simples, enquanto a categoria de logs de eventos distribuídos (no estilo Kafka) atende melhor quando o valor está no histórico ordenado e replayável de eventos, com múltiplos consumidores lendo o mesmo fluxo em ritmos independentes. A escolha entre categorias segue a mesma lógica do resto deste artigo: qual garantia e qual padrão de acesso o seu problema realmente exige.
Fechando#
Comunicação síncrona amarra dois serviços no tempo: os dois precisam estar de pé, ao mesmo tempo, no mesmo ritmo, ou a chamada falha. Mensageria quebra essa amarra inserindo um intermediário que guarda a mensagem até que o consumidor esteja pronto, desacoplando produtor de consumidor no tempo e no espaço. Esse desacoplamento é o que compra absorção de picos, resiliência a quedas do consumidor e escala horizontal por competing consumers — propriedades que uma chamada direta não tem como oferecer.
Escolha fila quando cada mensagem deve ser processada uma única vez por um entre vários workers; escolha pub/sub quando múltiplos interessados independentes precisam saber do mesmo evento; considere um log de eventos quando o histórico ordenado e replayável é, ele mesmo, o ativo de valor. Trate at-least-once como a garantia padrão e projete consumidores idempotentes por princípio, não como reação a um incidente de duplicação. Isole mensagens que nunca vão processar numa DLQ monitorada, em vez de deixá-las travar a fila atrás delas. E, ao publicar um evento como consequência de uma escrita no banco, use o padrão outbox para que os dois aconteçam atomicamente — ou nenhum vaza sozinho. Fila não é a ferramenta certa para toda comunicação entre serviços; é a ferramenta certa para a comunicação que não pode se dar ao luxo de exigir que os dois lados estejam de pé, ao mesmo tempo, no mesmo ritmo.