← Blog

Mensageria: por que filas melhoram o tempo de resposta

O cadastro “funciona”. O usuário clica em criar conta e espera. No servidor, a action grava o banco, monta o HTML do e-mail, abre SMTP, espera o provedor, só então devolve 200. Em um dia calmo, são dois segundos. No pico, o worker PHP está ocupado com SMTP enquanto outras requests enfileiram no FPM. O usuário culpa o “sistema lento”. O sistema está ocupado com correio.

Mensageria não é moda de diagrama. É separar intenção de trabalho lento. A API registra “envie este e-mail” e responde. Outro processo envia.

Síncrono: a request carrega o mundo

No desenho síncrono, tudo que o caso de uso dispara precisa terminar para a resposta existir. Banco, e-mail, PDF, HTTP para o ERP, thumbnail. O tempo de resposta é a soma. Qualquer terceiro lento vira lentidão sua. Qualquer timeout de terceiro vira erro na cara do usuário, mesmo com o pedido já gravado.

Há casos em que a soma é honesta. Débito de saldo, autorização de cartão, checagem de estoque no último item: o usuário precisa do resultado para seguir. Tirar isso da request não é arquitetura. É mentira.

O restante — “obrigado pelo pedido”, nota em PDF, índice de busca, recálculo de ranking — raramente precisa do clique. Colocar na request é conveniência de quem implementou o controller em 2019 e nunca mediu p95.

Enfileirar é rápido; enviar é que demora

Um broker (RabbitMQ, Redis, SQS, o que o time já opera com disciplina) aceita uma mensagem pequena em milissegundos. A latência de enqueue é baixa porque gravar a intenção é barato comparado a falar com SMTP, gerar relatório ou chamar parceiro. A API devolve 200 ou 202. O usuário segue. O worker consome, tenta, falha, tenta de novo.

Essa assimetria é o ganho de performance que a pessoa usuária sente. Não foi o PHP que ficou “mais rápido”. Foi o caminho crítico que perdeu o trecho lento. A métrica de tempo de resposta da API melhora porque a API parou de fazer o trabalho que não era dela no instante do clique.

O worker pode falhar sem desfazer o clique. Por isso a mensagem precisa ser idempotente: enviar o mesmo e-mail duas vezes não pode duplicar cobrança; gerar o mesmo relatório duas vezes pode sobrescrever o arquivo. Sem idempotência, a fila só espalha o bug com retentativa.

flowchart TD
  A[Usuário] --> B[API]
  B --> C[Grava o essencial]
  C --> D[Enqueue]
  D --> E[Resposta rápida]
  E --> A
  D --> F[Broker]
  F --> G[Worker]
  G --> H[E-mail, PDF, ERP]
  G --> I{Falhou?}
  I -->|Sim| J[Retry / DLQ]
  I -->|Não| K[Fim]
  classDef ouro fill:#FBF5E4,stroke:#C9A227,color:#0B1F18,stroke-width:1.4px
  classDef verde fill:#F3FAF7,stroke:#1E7A57,color:#0B1F18,stroke-width:1.4px
  classDef fim fill:#14513B,stroke:#0F3328,color:#FBFAF6,stroke-width:1.4px
  class A,B ouro
  class C,D,F,G,I verde
  class E,H,J,K fim

Horizontal nos workers, não só na API

Fila habilita o eixo certo de escala horizontal. A API cresce com cliques. Os consumidores crescem com o tamanho da fila. Se o marketing dispara milhares de e-mails, você sobe workers. O front e o checkout não precisam da mesma máquina extra.

Isso só vale se o worker for um processo separado de verdade — queue:work, consumidor, lambda, o nome que for — e se o broker não for “a tabela jobs no mesmo MySQL saturado” sem olhar lock. Implementação importa. O desenho é o mesmo: unidade de trabalho que se replica.

Relatório pesado entra no mesmo molde. O botão não gera o XLS na request. Publica “relatório X para o usuário Y”. A API responde “vamos enviar por e-mail”. O worker lê o banco, monta o arquivo, envia. Compliance que proíbe copiar o dado para Redis não proíbe processar depois; só proíbe o atalho de cache. Fila e cache não são a mesma ferramenta.

Trabalho Na request? Na fila?
Autorizar pagamento Sim Não como substituto da autorização
Enviar e-mail de boas-vindas Não Sim
Gerar PDF de 20 páginas Não Sim
Atualizar estoque do item vendido Quase sempre sim Só com consistência explícita
Recalcular ranking Não Sim
Chamar ERP lento Só se o clique depende Em geral sim, com status visível

O que a fila não apaga

Fila não aumenta a capacidade infinita do provedor de e-mail. Só protege o tempo de resposta da API. Se o broker cai e você não tem disco, retry ou fallback, o problema mudou de lugar. Se a mensagem é um blob enorme, você transportou o arquivo pelo caminho errado. Se ninguém observa o tamanho da fila e a idade da mensagem, o usuário recebe o “estamos processando” para sempre.

Contrato com o usuário precisa ser honesto. 200 com efeito colateral ainda não acontecido exige tela, e-mail ou polling. 201 com recurso incompleto exige estado: pendente, pronto, falhou. Esconder a assincronia na UX é a versão produto do e-mail síncrono: aparente simplicidade, suporte no dia seguinte.

Há também o anti-padrão da fila para tudo. Log de auditoria local, cálculo de 2 ms, validação do formulário: enqueue só adiciona falha. Mensageria paga um custo operacional. Use onde a espera é real e o acoplamento dói.

Como decidir na próxima feature

  1. O usuário precisa do resultado para o próximo clique?
  2. O trecho lento é I/O externo ou CPU longa?
  3. Posso repetir a mensagem sem duplicar efeito?
  4. Tenho como ver fila, retry e DLQ amanhã de manhã?

Se (1) é não e (2) é sim, a fila é candidata. Se (3) é não, desenhe idempotência antes. Se (4) é não, você não tem mensageria. Tem um dispatch no escuro.

Tempo de resposta é o que o clique sente. Fila não deixa o trabalho sumir. Coloca o trabalho no processo que pode crescer sem prender quem só queria um 200.

Se a fila virou o próximo problema do sistema, Mensageria One segue o caminho do primeiro publish até um pico em que o pedido não pode se perder.

Leia também: Escalabilidade vertical e horizontal e Cache: o que guardar e o que invalidar.

#Mensageria#Filas#Escalabilidade#Arquitetura

Thiago Nogueira

Autor de A Caixa Mágica e das séries Programming One e AI One na Editora Pensar Bem.

Capa de Python One

Python One

Do primeiro print à API REST em produção.