A maior parte do trabalho de backend se enquadra em duas categorias: trabalho em que um cliente está esperando no momento, e trabalho que apenas precisa acontecer eventualmente. O modelo produtor-consumidor é o padrão por trás dessa segunda categoria. Uma parte de um sistema produz unidades de trabalho e as entrega; outra parte as consome de forma independente, em seu próprio cronograma; e um buffer durável entre os dois absorve a incompatibilidade de seus ritmos. Filas, workers, BullMQ, SQS e até mesmo jobs acionados por cron são implementações específicas dessa única ideia subjacente.
Esta página é o modelo mental por trás do restante da seção - por que o desacoplamento do produtor do consumidor importa, o que um broker realmente garante sobre a entrega (e o que não garante), e o vocabulário que as páginas práticas assumem que você já possui. Noções Básicas de Filas mostra este padrão em código funcional de Express e BullMQ; esta página é sobre a forma subjacente a ele.
Um produtor entrega trabalho a um broker durável em vez de executá-lo inline, e um ou mais consumidores (workers) retiram esse trabalho de forma independente - desacoplando o ritmo do tratamento de requisições do ritmo do processamento.
Por que Importa: Sem esse buffer, cada trabalho lento ou com picos de chegada precisa acontecer dentro da requisição que o acionou, o que significa que a latência voltada para o usuário herda o pior caso de qualquer coisa que seja mais lenta no downstream.
Conceitos Chave:produtor, consumidor/worker, broker, backpressure, garantia de entrega, fila de mensagens mortas.
Quando Usar: Trabalho que é lento em relação ao orçamento de latência de uma requisição, trabalho que deve sobreviver a uma falha ou deploy, trabalho com padrões de chegada em picos que, de outra forma, sobrecarregariam os sistemas downstream, e trabalho que legitimamente não precisa ser concluído antes de responder ao chamador.
Limitações / Trade-offs: Você troca consistência imediata e depuração simples por resiliência e escalabilidade independente - um sistema em fila tem mais partes móveis, conclusão eventual em vez de imediata, e garantias de entrega que são mais fracas do que parecem.
Tópicos Relacionados: idempotência no consumidor, retentativas e backoff, agendamento e cron, circuit breakers.
Imagine uma cozinha de restaurante com um trilho de tickets entre o salão e a linha de produção. Um garçom anota um pedido, escreve um ticket e o prende no trilho - e então volta imediatamente para a próxima mesa sem esperar que a comida seja cozida. Os cozinheiros pegam os tickets do trilho sempre que terminam seu prato atual, trabalham neles na ordem aproximada em que chegaram, e ninguém no salão é bloqueado pelo ritmo de nenhum cozinheiro. O trilho é o buffer que torna possível que a anotação de pedidos e o cozimento ocorram em duas velocidades completamente diferentes sem que um estagne o outro.
Esse é o modelo produtor-consumidor. O produtor é a parte do seu sistema que descobre que um trabalho precisa ser feito - um manipulador HTTP que acabou de aceitar uma requisição, por exemplo - e seu único trabalho é descrever esse trabalho e entregá-lo. O broker é o trilho durável em si: uma fila que mantém cada unidade de trabalho (um job ou mensagem) até que algo esteja pronto para processá-la, sobrevivendo a uma reinicialização de qualquer um dos lados. O consumidor, frequentemente chamado de worker, é um processo separado que retira jobs do broker e faz o trabalho real, em seu próprio cronograma e frequentemente como um deploy completamente diferente do produtor.
A mudança importante é que o produtor para de esperar. Um manipulador HTTP que enfileira um job pode responder ao cliente em milissegundos - tipicamente com um 202 Accepted e um ID de job como recibo - em vez de bloquear pelo tempo que o trabalho real leva. O cliente recebe uma resposta imediata e honesta ("Eu aceitei isso"), e o processamento real acontece em um cronograma completamente desacoplado.
A principal diferença mecânica de uma chamada de função normal é que um produtor não obtém um valor de retorno - ele obtém um recibo. Algo como isso é todo o contrato:
// Produtor: entrega uma descrição do trabalho, recebe um ID imediatamenteconst job = await emailQueue.add("send-invite", { to, orgId });// job.id é um recibo, não um resultado - o trabalho ainda não aconteceu
O que o broker promete sobre a entrega desse job é a parte que as pessoas mais frequentemente erram. Filas distribuídas oferecem três garantias teóricas: no máximo uma vez (um job pode desaparecer silenciosamente, mas nunca é executado duas vezes), pelo menos uma vez (um job tem a garantia de ser tentado, mas pode ser executado mais de uma vez), e exatamente uma vez (cada job é executado precisamente uma vez, sem duplicatas, sem perdas). Quase todos os brokers reais - BullMQ, SQS e a maioria dos outros - usam por padrão "pelo menos uma vez", porque "exatamente uma vez" requer a coordenação do broker e dos efeitos colaterais do consumidor como uma única transação atômica, o que não é alcançável através de uma rede no caso geral. É por isso que Chaves de Idempotência existe como sua própria página: a entrega "pelo menos uma vez" transfere a responsabilidade pela correção para o consumidor, que deve tratar "processado este job duas vezes" como um caso esperado, não um caso de borda.
O mecanismo que faz "pelo menos uma vez" funcionar é um lease, às vezes chamado de timeout de visibilidade: quando um worker pega um job, o broker o esconde de outros workers por uma janela limitada em vez de excluí-lo imediatamente. Se o worker confirmar a conclusão antes que o lease expire, o job é removido permanentemente. Se não - porque o worker falhou, ou o processo foi encerrado no meio do job, ou o deploy rolou o pod - o lease expira e o broker torna o job visível novamente para que outro worker o pegue. Essa é a mecânica por trás de "pelo menos uma vez": o job retorna precisamente porque o broker não consegue distinguir um worker lento de um worker morto.
Backpressure é o outro lado do desacoplamento. Uma fila suaviza picos, mas não é infinita - a profundidade da fila (quantos jobs estão esperando) é o sinal que informa se os consumidores estão acompanhando os produtores. Uma profundidade que cresce constantemente significa que os consumidores estão ficando para trás, e ao contrário de um sistema síncrono onde isso aparece imediatamente como timeouts, uma fila pode mascarar o problema por um tempo absorvendo o backlog - que é exatamente por que a profundidade da fila pertence a um dashboard ao lado da concorrência do worker, e não para aparecer como um mistério horas depois.
Como produtor e consumidor são desacoplados, eles escalam independentemente e em eixos diferentes. Produtores - tipicamente sua camada de API - escalam com o volume de requisições; consumidores escalam com o volume e custo do trabalho em si, e um consumidor com uso intensivo de CPU (redimensionamento de imagem) muitas vezes precisa de uma forma de instância completamente diferente de um leve (envio de e-mail). É aqui também que a ordenação se torna sutil: uma fila FIFO de partição única preserva a ordem estrita, mas limita a taxa de transferência a um worker por vez para essa partição, enquanto uma fila padrão (não FIFO) troca a ordenação estrita por consumo paralelo em muitos workers. Filas de prioridade ficam na mesma tensão - pular jobs urgentes para a frente da fila é fácil de adicionar e fácil de abusar, e um fluxo ilimitado de trabalho "urgente" irá privar tudo mais exatamente da mesma forma que uma fila de prioridade não gerenciada faria na cozinha.
Jobs que falham repetidamente precisam de um lugar para ir além de serem retentados para sempre ou descartados silenciosamente - uma fila de mensagens mortas (DLQ) captura mensagens que excedem seu orçamento de retentativas para que um humano possa inspecionar o que uma "mensagem venenosa" realmente continha, em vez de perdê-la ou ficar em loop indefinidamente.
O agendamento merece uma menção aqui porque é fácil pensar nele como um conceito separado quando, na verdade, é o mesmo modelo com um gatilho diferente: um job acionado por cron é um produtor cujo evento é "o relógio atingiu esta hora" em vez de "um usuário fez esta coisa". Agendamento & Cron cobre a complicação operacional que vem com ele - sem um lock de líder, cada réplica de um produtor escalado dispara o mesmo job agendado redundantemente.
Estilo de Broker
Força
Fraqueza
Melhor Ajuste
Baseado em Redis (BullMQ)
Baixa latência, recursos ricos de job (prioridade, atraso, retentativas) prontos para uso
Você executa e opera o Redis por conta própria; não construído para durabilidade entre regiões
Jobs em background no nível do aplicativo, baixo a médio throughput
Fila gerenciada na nuvem (SQS)
Durabilidade e escalabilidade totalmente gerenciadas, sem broker para operar
Conjunto de recursos mais grosseiro; precificação por requisição e latência de rede adicionada
Sistemas nativos da nuvem já na AWS, altas necessidades de durabilidade
Baseado em Log (estilo Kafka)
Histórico reproduzível, muitos grupos de consumidores independentes sobre o mesmo stream
Pegada operacional mais pesada; não projetado como uma simples fila de tarefas
Streaming de eventos, trilhas de auditoria, várias equipes lendo os mesmos eventos
"Uma fila garante processamento exatamente uma vez." Quase nenhuma faz isso por padrão - a maioria garante pelo menos uma vez, o que significa que seu consumidor precisa ser idempotente, não o broker.
"Se a fila aceitou o job, o trabalho está basicamente feito." A aceitação significa apenas que o job foi gravado de forma durável - não diz nada sobre se ou quando um worker realmente o conclui.
"A profundidade da fila crescendo um pouco está bem, desde que eventualmente se limpe." Uma fila que nunca se esvazia completamente durante o tráfego normal é um indicador principal de que os consumidores estão subdimensionados, não um soluço autocorretivo.
"A ordenação FIFO é o comportamento padrão de uma fila." Filas padrão na maioria dos brokers trocam deliberadamente a ordenação estrita por paralelismo; você precisa optar pelas semânticas FIFO e aceitar seu teto de throughput.
"Workers precisam viver no mesmo processo ou container que a API." O objetivo do modelo é que eles não precisam - produtor e consumidor são tipicamente deployáveis separados escalados por sinais diferentes.
Qual é a diferença entre uma "fila" e um "broker"?
Eles são frequentemente usados de forma intercambiável, mas estritamente falando, um broker é a peça de infraestrutura (Redis, SQS, Kafka) que gerencia o armazenamento e a entrega de mensagens, enquanto uma fila é um canal nomeado de jobs dentro dele - um único broker pode hospedar muitas filas separadas.
Por que não processar tudo síncronamente e escalar a API em vez disso?
Escalar réplicas de API ajuda com o throughput, mas não corrige a latência para operações lentas individuais, não sobrevive a uma falha no meio da operação e não permite dimensionar a computação de forma diferente para requisições leves versus trabalho pesado em background.
O que "entrega pelo menos uma vez" realmente significa na prática?
Significa que o broker garante que um job será tentado pelo menos uma vez, mas uma falha, timeout ou expiração do lease pode fazer com que ele seja tentado novamente - então os efeitos colaterais do seu worker precisam ser seguros para repetir, não apenas seguros para executar uma vez.
Como um broker sabe que um worker ainda está processando um job e não falhou?
Ele não sabe, diretamente - ele depende de um lease (timeout de visibilidade): o worker deve confirmar a conclusão antes que o lease expire, e se não o fizer, o broker assume o pior e torna o job disponível para outro worker.
A entrega exatamente uma vez é possível?
Não no sentido distribuído geral - alcançá-la exigiria que o broker e o efeito colateral do consumidor se comprometessem como uma única operação atômica através de uma rede, o que não está praticamente disponível. Sistemas que afirmam isso estão quase sempre fazendo entrega pelo menos uma vez mais deduplicação.
O que devo realmente observar para saber se meus workers estão acompanhando?
A profundidade da fila ao longo do tempo e a idade do job (há quanto tempo o job mais antigo esperando está lá) - uma profundidade plana ou decrescente com baixa idade do job significa que os consumidores estão acompanhando; uma profundidade que sobe constantemente significa que não estão.
Por que mensagens venenosas precisam de uma fila de mensagens mortas em vez de apenas retentar para sempre?
Um loop de retentativa ilimitado em um job que nunca pode ter sucesso desperdiça a capacidade do worker indefinidamente e pode privar jobs saudáveis que estão atrás dele - uma DLQ limita o orçamento de retentativas e preserva a mensagem para inspeção em vez de perdê-la.
As filas de prioridade são seguras para confiar?
Elas são úteis com moderação, mas todo job marcado como "prioritário" está implicitamente despriorizando todo o resto - se muito tráfego for marcado como urgente, jobs de baixa prioridade podem ficar sem recursos indefinidamente, o que anula o propósito de ter prioridades.
Um job cron também é um padrão produtor-consumidor?
Sim - o agendador atua como o produtor, disparando em um gatilho baseado em tempo em vez de um evento do usuário, e o que quer que execute o job ainda é um consumidor retirando (ou recebendo) essa unidade de trabalho.
Por que o processamento baseado em fila muda a forma como penso sobre falhas?
Porque as falhas se tornam explícitas e inspecionáveis - um job falho fica em algum lugar (retentando, ou em uma DLQ) em vez de desaparecer em uma resposta 500 que o chamador tem que interpretar e retentar por conta própria.
Produtor e consumidor precisam ser escritos na mesma linguagem ou framework?
Não - eles só precisam concordar com o protocolo do broker e a forma do payload do job, que é uma razão pela qual o modelo funciona bem para sistemas poliglotos onde, digamos, uma API Node enfileira trabalho que um serviço worker separado em outra stack consome.
Quando uma fila é a ferramenta errada?
Quando o chamador genuinamente precisa do resultado antes de responder - o enfileiramento adiciona uma viagem de ida e volta e semânticas de conclusão eventual que fazem sentido para trabalho em background, mas adicionam latência e complexidade desnecessárias a uma requisição que é inerentemente síncrona.