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
- O usuário precisa do resultado para o próximo clique?
- O trecho lento é I/O externo ou CPU longa?
- Posso repetir a mensagem sem duplicar efeito?
- 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.