Por que entregar um webhook de forma confiável é mais difícil do que parece
Todo mundo que já integrou um gateway de pagamento (Kiwify, Hotmart, Stripe, Asaas) conhece o roteiro: a plataforma manda um POST pro seu endpoint avisando "pedido aprovado", e o seu sistema libera o acesso do cliente. Simples. Até o dia em que um cliente paga e não recebe nada, e você descobre que aquele webhook se perdeu no meio do caminho.
O que parece um POST trivial esconde um problema clássico de sistemas distribuídos: entrega confiável entre dois sistemas que falham em momentos diferentes. Este artigo destrincha os pontos que tornam isso difícil e como se resolve cada um. No fim, comento a ferramenta que estou construindo em cima dessas ideias.
O ingênuo: receber e processar
A primeira versão que todo mundo escreve é assim:
POST /webhook -> valida -> libera acesso -> envia e-mail -> responde 200
O problema aparece rápido. Se "libera acesso" e "envia e-mail" demoram, ou dependem de uma API externa lenta, a resposta 200 demora junto. E o remetente (o gateway) não espera pra sempre: ele tem um timeout. Se você não confirma a tempo, ele considera falha, mesmo que seu processamento termine depois. Resultado: do lado dele, "falhou"; do seu lado, pode ter processado, ou não. Ambiguidade.
Ponto 1: responder rápido, processar depois
A primeira correção é desacoplar recepção de processamento:
POST /webhook -> grava o evento cru -> responde 200 imediatamente
(em segundo plano) worker -> lê o evento -> processa
O endpoint agora faz só duas coisas: persistir o payload e responder. Milissegundos. O trabalho pesado sai da frente e roda num worker separado, no seu ritmo. O gateway recebe o 2xx rápido e fica feliz.
Isso introduz a necessidade de uma fila (ou de uma tabela de eventos funcionando como fila). E fila traz os próximos problemas.
Ponto 2: idempotência (o cliente não pode receber o produto duas vezes)
Gateways reenviam webhooks quando acham que falharam. Isso é bom (não perde evento), mas significa que você vai receber o mesmo evento mais de uma vez. Se o seu worker processa cada entrega cegamente, o cliente recebe o produto duas vezes, ou toma dois e-mails, ou você conta a venda em dobro.
A solução é idempotência: cada evento precisa de uma chave estável, e você processa cada chave uma única vez. Um detalhe que pega muita gente: use um identificador do recurso (o id do pedido/venda), não um id da tentativa de entrega. Na Kiwify, por exemplo, o id do "envelope" muda a cada reenvio, então ele não serve pra deduplicar. No banco, um UNIQUE na chave de idempotência resolve de forma limpa: a segunda inserção do mesmo evento simplesmente não entra.
Ponto 3: retry com backoff (o servidor de destino também cai)
Até aqui protegemos contra o nosso endpoint ser lento. Mas o worker, ao entregar o evento pro destino final (o sistema do cliente), enfrenta o mesmo problema ao contrário: e se o destino estiver fora do ar?
Reentregar na hora, em loop, é a pior ideia: você martela um servidor que já está sofrendo. A resposta é backoff exponencial: falhou, espera um pouco e tenta de novo, aumentando o intervalo a cada tentativa (1min, 5min, 15min, 1h). Isso dá tempo do destino se recuperar sem ser bombardeado. E, depois de N tentativas sem sucesso, o evento vai pra uma "fila morta" (Dead Letter Queue) pra inspeção manual, em vez de tentar pra sempre.
Ponto 4: visibilidade e replay
Quando algo dá errado (e vai), você precisa enxergar. Sem log do payload que entrou, do que saiu e da resposta exata do destino, você fica no escuro. Guardar isso permite duas coisas valiosas: diagnosticar ("o servidor devolveu 500 nesse header") e reprocessar em massa (o destino ficou 2h fora, você reenvia todos os eventos daquele período de uma vez, em vez de um por um).
Ponto 5: aguentar o pico
Tudo isso é testado no pior momento: a Black Friday ou o lançamento, quando chegam milhares de eventos em minutos. O endpoint de ingestão precisa ser leve o suficiente pra absorver o pico sem cair (por isso ele só persiste e responde), e a fila precisa segurar o acúmulo enquanto o worker drena no seu ritmo. Desacoplar recepção de processamento não é elegância acadêmica: é o que mantém tudo de pé quando a carga sobe.
Juntando as peças
Uma arquitetura confiável de webhook, no mínimo, tem: um endpoint de ingestão que persiste e responde 2xx na hora; uma fila entre recepção e processamento; idempotência por chave estável; retry com backoff exponencial e uma DLQ; e logs que permitam diagnóstico e replay. Nenhuma dessas peças é exótica sozinha. A dificuldade é que você precisa de todas juntas, corretas, pra não perder um evento, e mantê-las é trabalho contínuo.
Foi partindo disso que comecei a construir o HookSafe: uma camada que fica entre o gateway e o seu servidor, responde 2xx na hora, e cuida de fila, retry, idempotência e replay pra você, sem precisar reescrever o seu sistema. Está em early access. Se o tema te interessa, o problema é mais fundo do que parece, e é um puta exercício de engenharia.
Como você resolve entrega confiável de webhook nos seus projetos? Curioso pra saber as abordagens de vocês nos comentários.
Top comments (1)
Excellent overview. Reliable webhook processing is less about the endpoint itself and more about the architecture behind it. Queue-first processing, retries, and idempotency turn an unreliable network into a resilient system. These patterns are easy to overlook early on but become essential as traffic and complexity grow.