Entrega confiável de eventos: retry, backoff e fila para não perder conversão
Como uma fila, retry com backoff exponencial e dead-letter transformam envios de conversão que falham em silêncio em uma cadeia que não perde nem um evento.
Neste artigo
Toda camada de conversão server-side tem um momento em que a rede falha. A API da plataforma devolve 503, o timeout estoura, o DNS pisca por dois segundos. Numa integração ingênua, esse momento é onde a conversão morre: o código tentou enviar, a chamada falhou, a exceção foi engolida por um try distraído, e o evento simplesmente não existe mais. Ninguém percebe, porque não há erro visível — há apenas um Purchase que nunca chegou, uma otimização de campanha alimentada com menos dados do que deveria, e um relatório levemente mais pobre que o real. A falha de entrega mais cara é a que não faz barulho.
Enviar eventos de forma confiável não é sobre a chamada dar certo. É sobre o que acontece quando ela dá errado. E como ela vai dar errado — não é hipótese, é certeza estatística ao longo de milhares de envios —, a arquitetura precisa assumir a falha como estado normal e ter um plano para cada evento que não passou de primeira.
Por que o envio síncrono direto é frágil#
O padrão que quase todo mundo escreve primeiro é o envio síncrono e direto: o evento acontece, o código chama a API da plataforma na hora, no mesmo fluxo, e segue em frente. Ele tem três fragilidades que se manifestam sob carga.
A primeira é o acoplamento à disponibilidade da plataforma: se a API do destino está lenta ou fora, o seu fluxo trava esperando ou falha junto. Um problema deles vira um problema seu.
A segunda é a perda sob falha transitória: uma oscilação de dois segundos, que uma simples nova tentativa resolveria, derruba o evento porque não há nova tentativa.
A terceira é a falta de visibilidade: sem um lugar onde o evento fica registrado até ser confirmado, você não sabe quantos falharam nem consegue reenviá-los. O evento perdido não deixa rastro.
A correção estrutural para as três é a mesma: desacoplar o acontecimento do envio com uma fila no meio.
A fila como espinha dorsal#
Em vez de enviar na hora, você enfileira o evento assim que ele acontece e responde imediatamente a quem o originou (o webhook, a requisição). Um ou mais workers consomem a fila e fazem o envio real à plataforma, no seu próprio ritmo, com sua própria política de falha.
Esse desacoplamento muda tudo:
- Absorve indisponibilidade — se a plataforma cai, os eventos se acumulam na fila e são enviados quando ela volta. Nada se perde por causa de uma queda temporária do destino.
- Suaviza picos — uma rajada de vendas na Black Friday enche a fila, e os workers drenam no ritmo que a API do destino aguenta, sem estourar limites nem derrubar o seu sistema.
- Cria um ponto de controle — a fila é onde você mede backlog, taxa de sucesso e latência de entrega. O que estava invisível fica mensurável.
A fila introduz a garantia at-least-once (o evento é entregue pelo menos uma vez, pode repetir), o que torna a idempotência do consumidor um pré-requisito — mas essa é a troca certa: prefira duplicar e desduplicar do que perder.
Retry com backoff exponencial#
Dentro do worker, a peça central é a política de retry. Quando um envio falha, você tenta de novo — mas não de qualquer jeito.
Repetir imediatamente e em loop é contraproducente: se a plataforma está sobrecarregada, uma enxurrada de retries instantâneos piora a sobrecarga (o efeito manada). A técnica correta é o backoff exponencial: cada nova tentativa espera mais que a anterior. A primeira falha espera, digamos, 1 segundo; a segunda, 2; a terceira, 4; depois 8, 16, e assim por diante, até um teto.
`` tentativa 1 -> falhou -> espera ~1s tentativa 2 -> falhou -> espera ~2s tentativa 3 -> falhou -> espera ~4s tentativa 4 -> falhou -> espera ~8s ...até um máximo de tentativas ou um teto de espera ``
A isso se soma o jitter: uma variação aleatória adicionada ao tempo de espera. Sem jitter, mil eventos que falharam no mesmo instante vão tentar de novo exatamente no mesmo instante, recriando o pico. Com jitter, as retentativas se espalham no tempo, e a plataforma se recupera em vez de apanhar de novo. Backoff diz "espere mais a cada falha"; jitter diz "e não combine com os outros".
Distinguir o que dá para repetir do que não dá#
Nem toda falha merece retry, e insistir na falha errada desperdiça recurso ou corrompe dado. A distinção é entre erros transitórios e permanentes.
- Transitórios (repetir) —
429(limite de taxa),500/502/503/504(erro/instabilidade do servidor), timeouts, falhas de conexão. São problemas do momento; a mesma requisição tem boa chance de funcionar depois. - Permanentes (não repetir) —
400(payload malformado),401/403(credencial inválida),422(validação). Repetir não muda o resultado: a requisição está errada, e mil tentativas vão falhar mil vezes. Esses eventos precisam de correção, não de insistência.
Um caso especial é o 429: muitas APIs devolvem um cabeçalho Retry-After dizendo quanto esperar. Respeitá-lo é mais educado — e mais eficaz — do que aplicar o backoff genérico.
Dead-letter: onde o evento vai quando desiste#
O retry não pode ser infinito. Um evento com payload permanentemente inválido, ou um destino que ficou horas fora, precisa de um destino final quando as tentativas se esgotam. Esse destino é a dead-letter queue (DLQ): uma fila separada para onde vão os eventos que falharam além do limite.
A DLQ é o oposto de perder o evento em silêncio. Em vez de desaparecer, o evento fica parqueado com o histórico da falha (código de erro, número de tentativas, payload). A partir dela você pode:
- Alertar — uma DLQ que cresce é um sinal operacional de que algo está quebrado (credencial vencida, mudança de contrato da API), e deve disparar aviso no monitoramento.
- Inspecionar — olhar o que falhou revela a causa raiz.
- Reprocessar — corrigido o problema, os eventos podem ser reenfileirados. Como o consumo é idempotente, reprocessar é seguro.
Sem DLQ, "esgotou os retries" significa "perdeu para sempre". Com DLQ, significa "guardado para tratar depois". É a diferença entre uma falha recuperável e uma perda definitiva.
A cadeia completa#
Juntando as peças, a cadeia confiável de entrega de um evento de conversão fica assim:
- O evento acontece (webhook, ação no site) e é enfileirado imediatamente; a origem recebe resposta rápida.
- Um worker consome a fila e tenta enviar à plataforma.
- Falha transitória dispara retry com backoff exponencial e jitter, respeitando
Retry-Afterquando houver. - Falha permanente não é repetida; vai direto para tratamento.
- Esgotado o limite de tentativas, o evento vai para a dead-letter queue, que alerta e permite reprocessamento.
- Todo o consumo é idempotente, então retries e reprocessamentos nunca duplicam a conversão.
O que medir#
Uma cadeia confiável é também uma cadeia observável. Sem métricas, você troca a cegueira do envio direto pela cegueira da fila. Acompanhe:
- Backlog da fila — quantos eventos esperam. Crescimento sustentado indica que os workers não dão conta ou que o destino está degradado.
- Taxa de sucesso na primeira tentativa — quão saudável está a integração.
- Distribuição de retries — muitos eventos precisando de várias tentativas é sinal de instabilidade.
- Tamanho da DLQ — idealmente zero; qualquer crescimento é incidente.
- Latência de entrega — do acontecimento até a confirmação da plataforma; janelas de atribuição têm prazo, e um atraso grande pode fazer a conversão chegar tarde demais para ser creditada.
O envio parcial: o pior tipo de falha#
Há uma falha mais insidiosa que a chamada que falha por inteiro: a que falha pela metade. Você envia o evento, a plataforma o recebe e processa, mas a resposta se perde no caminho de volta — a conexão cai depois que o destino já contou a conversão, mas antes de o seu worker receber o 200. Do ponto de vista do worker, a chamada falhou; a política de retry entra em ação e reenvia. Agora a conversão foi contada duas vezes no destino, e o seu sistema nem sabe.
Esse cenário é a razão pela qual retry exige idempotência no destino, não apenas no consumidor. O event_id estável enviado à plataforma é o que a permite reconhecer o reenvio como duplicata do que ela já contou. Sem ele, a combinação de retry com envio parcial produz exatamente a contagem dupla que a fila deveria evitar. A lição é que confiabilidade e idempotência não são temas separados: uma cadeia de entrega que reenvia sem um identificador de deduplicação não é robusta, é uma máquina de duplicar sob falha ambígua.
Ordenação e eventos dependentes#
Filas com múltiplos workers processando em paralelo não garantem ordem. Dois eventos do mesmo pedido — um checkout e o purchase que o segue — podem ser tratados fora de sequência, e um evento que depende do estado deixado por outro chega antes da hora. Na mensuração de conversão isso costuma ser tolerável, porque cada evento carrega seu próprio contexto e não depende do anterior. Mas quando existe dependência real, duas estratégias resolvem:
- Particionar por chave — garantir que todos os eventos de um mesmo pedido caiam na mesma partição/fila e sejam processados em ordem, ao custo de menos paralelismo para aquela chave.
- Tornar cada evento autossuficiente — carregar no próprio evento tudo de que ele precisa, para que a ordem de chegada deixe de importar. É a abordagem preferível quando possível, porque preserva o paralelismo.
Escolher entre elas é escolher entre ordem e vazão; a maioria das cadeias de conversão vive bem com eventos autossuficientes e sem garantia de ordem.
Confirmar de verdade: só remova da fila quando a plataforma aceitou#
Um erro sutil desmonta toda a confiabilidade construída até aqui: remover o evento da fila antes de ter certeza de que ele foi aceito. Se o worker tira a mensagem da fila e só depois tenta o envio, uma queda no meio faz o evento evaporar — ele não está mais na fila e não chegou ao destino. A regra correta é a confirmação explícita: a mensagem só é reconhecida (e removida) depois que a plataforma respondeu com sucesso. Se o worker cai antes disso, a mensagem volta a ficar visível na fila e é reprocessada por outro — de novo, at-least-once fazendo seu trabalho, e a idempotência garantindo que o reprocessamento não duplique.
Esse detalhe é a diferença entre uma fila que garante entrega e uma que só parece garantir. Vale a checagem explícita: no seu consumidor, o ack acontece na linha depois do envio confirmado, nunca antes.
Não deixe a cadeia inteira depender de um destino#
Uma última disciplina de robustez: o envio para uma plataforma nunca deve poder derrubar o envio para as outras, nem travar a cadeia inteira. Se a Meta está fora, o Purchase para o Google e para o TikTok tem de sair normalmente; o evento da Meta fica retido na sua fila (ou vai para a dead-letter) e é reenviado quando ela volta. Isolar os destinos — uma fila ou um fluxo de retry por plataforma — impede que a indisponibilidade de um contamine os demais. É a mesma lógica de independência que se aplica a serviços: a queda de um vizinho degrada aquela integração específica, jamais toda a mensuração.
Entrega confiável de eventos é, no fundo, a recusa a aceitar que "a chamada falhou" seja o fim da história. Com uma fila para desacoplar, retry com backoff e jitter para vencer o transitório, distinção entre falha temporária e definitiva, e uma dead-letter que transforma desistência em pendência recuperável, cada evento de conversão tem um caminho até o destino — mesmo quando o primeiro caminho falha. A conversão que você não perde é a que continua rendendo otimização, e ela vale muito mais do que a simplicidade do envio direto que a deixaria escapar.