arquitetura · eventos

A fila não escala seu ERP. Ela impede que você perca o pedido.

Versão técnica. Se você é dono de empresa e quer entender o que acontece na promoção, leia Por que os pedidos somem na promoção (4 min).

O cenário se repete: a empresa dispara uma campanha, o WhatsApp recebe em minutos o volume de um dia inteiro, e o ERP cai. Alguém sugere RabbitMQ, a fila entra, o ERP para de cair — e o time conclui que a fila "escalou" o sistema.

Não escalou. A capacidade do ERP continua exatamente a mesma. O que a fila fez foi trocar um problema por outro, e o novo problema é muito melhor. Vale entender a troca com precisão, porque ela define o que você precisa dizer ao cliente.

1. O que quebra na integração síncrona

Na integração direta, cada mensagem que chega vira uma chamada ao ERP na hora. Enquanto o volume é menor que a capacidade, funciona. No pico, três coisas acontecem em sequência:

  1. As chamadas se acumulam em execução simultânea e o tempo de resposta sobe.
  2. O ERP atinge o limite de conexões e passa a recusar — ou pior, aceita e trava.
  3. O canal que originou a chamada estoura o timeout. O pedido não existe em lugar nenhum: não foi gravado e o cliente recebeu erro.

O passo 3 é o que custa dinheiro. Não é lentidão — é perda. Para dimensionar, simulei uma campanha: 60s de tráfego normal, 60s de pico (disparo de lista), 60s de normalização, contra um ERP que processa 5 pedidos/s e recusa acima de 20 chamadas simultâneas.

eda.py · simulação determinística✓ testado
def sincrono(c: Carga) -> dict:
    """Cada pedido chama o ERP na hora. Acima do limite, o ERP recusa."""
    em_voo = 0; perdidos = 0; processados = 0
    for novos in c.chegadas:
        em_voo = max(0, em_voo - c.erp_por_segundo)
        processados += min(em_voo + novos, c.erp_por_segundo)
        espaco = c.concorrencia_max - em_voo
        aceitos = min(novos, max(0, espaco))
        perdidos += novos - aceitos          # pedido que nunca existiu
        em_voo += aceitos
    return {"processados": processados, "perdidos": perdidos}


def com_fila(c: Carga) -> dict:
    """Pedidos vão para a fila. O consumidor drena no ritmo do ERP."""
    fila = 0; processados = 0; espera_max = 0; segundo = 0
    for novos in c.chegadas:
        fila += novos
        vazao = min(fila, c.erp_por_segundo)
        fila -= vazao; processados += vazao; segundo += 1
        espera_max = max(espera_max, fila / c.erp_por_segundo)
    while fila > 0:                        # drena o resto depois do pico
        vazao = min(fila, c.erp_por_segundo)
        fila -= vazao; processados += vazao; segundo += 1
    return {"processados": processados, "perdidos": 0,
            "espera_max_s": round(espera_max, 1), "fim_s": segundo}
saída real da execução✓ 2.640 pedidos simulados
pedidos recebidos:      2640
SINCRONO  processados:  555 | PERDIDOS: 2085 (79%)
COM FILA  processados: 2640 | PERDIDOS:    0 | espera maxima: 420.0s | fim aos 564s

2. A troca, dita com clareza

A integração síncrona perdeu 79% dos pedidos. A fila não perdeu nenhum. Mas repare na outra coluna: a espera máxima chegou a 420 segundos, e a fila só terminou de drenar aos 564s — o pico durou 60s.

A fila não criou capacidade. Um ERP de 5 pedidos/s continua sendo de 5 pedidos/s; 2.640 pedidos levam no mínimo 528 segundos, e a simulação terminou em 564. O que a fila fez foi converter perda em espera — e espera é um problema que se administra, enquanto pedido perdido é receita que não volta.

Isso tem uma consequência de produto que costuma passar batido: se o pedido vai demorar 7 minutos para entrar no ERP, a confirmação ao cliente não pode depender do ERP. Ela tem que sair no momento em que a mensagem entra na fila — "recebemos seu pedido" — e a confirmação definitiva vira um segundo aviso. Adotar fila sem mudar essa comunicação troca "deu erro" por "ficou mudo", que é pior.

E se a espera de 7 minutos for inaceitável para o negócio, a resposta não é mexer na fila: é aumentar a vazão do consumidor ou do ERP. A fila só torna o gargalo visível e mensurável — o que já é muito, porque agora ele é um número, e não um incidente.

3. Entrega ao menos uma vez: o duplicado é regra, não exceção

RabbitMQ garante entrega ao menos uma vez. Se o consumidor processa e cai antes de confirmar, a mensagem volta para a fila e é entregue de novo. Sem proteção, isso é pedido duplicado no ERP — e cliente cobrado duas vezes.

A proteção não é de infraestrutura, é de aplicação: o consumidor precisa ser idempotente, com chave de negócio persistida.

consumidor idempotente✓ testado
def deduplicar(eventos: list[tuple[str, str]]) -> list[str]:
    """RabbitMQ entrega ao menos uma vez: o consumidor precisa ser idempotente."""
    vistos: set[str] = set()
    aplicados: list[str] = []
    for chave, pedido in eventos:
        if chave in vistos:
            continue
        vistos.add(chave)
        aplicados.append(pedido)
    return aplicados

# entregas recebidas: 5  ->  aplicadas: 3  ['A', 'B', 'C']

Em produção o set vira uma constraint UNIQUE na chave de idempotência, dentro da mesma transação que grava o pedido. Guardar a chave em memória do processo não protege: o processo que caiu é justamente o que perdeu a memória.

4. Quando não usar fila

Fila adiciona um componente para operar, monitorar e recuperar. Ela não se justifica quando:

  • A resposta precisa ser imediata e exata — consulta de saldo, validação de cupom, autorização de pagamento. Assíncrono aqui só empurra o problema para a interface.
  • O volume nunca se aproxima da capacidade. Se o pico é 3 pedidos/s contra um ERP de 50/s, a fila resolve um problema que você não tem.
  • Não há quem opere. Fila sem monitoramento de profundidade e sem fila de mensagens mortas (DLQ) transforma perda visível em perda silenciosa — o pior dos dois mundos.

O critério é o mesmo que aplicamos em escolha de ferramenta: componente novo só entra quando o que existe comprovadamente não resolve.

5. O resumo

Fila não aumenta a capacidade do seu ERP — converte pedido perdido em pedido atrasado. Essa troca quase sempre vale (79% de perda contra 0%), mas exige duas decisões de produto: confirmar o recebimento sem esperar o ERP, e tornar o consumidor idempotente porque o duplicado vai acontecer. Sem essas duas, a fila só esconde o gargalo.

próximo passo

Seu canal de vendas aguenta o próximo disparo?

Diagnóstico sem custo: medimos a capacidade real do seu ERP, simulamos o pico da sua campanha e dizemos se fila resolve ou se o gargalo é outro.