Checklist de confiabilidade de webhooks: retries, idempotência, replay
Checklist prático de confiabilidade para webhooks: retries, idempotência, logs de replay e monitoramento para webhooks inbound e outbound quando parceiros falham.

Por que webhooks parecem pouco confiáveis em projetos reais
Um webhook é um acordo simples: um sistema envia uma requisição HTTP para outro sistema quando algo acontece. "Pedido enviado", "ticket atualizado", "dispositivo saiu do ar". É basicamente uma notificação push entre apps, entregue pela web.
Eles parecem confiáveis em demos porque o caminho feliz é rápido e limpo. No trabalho real, webhooks ficam entre sistemas que você não controla: CRMs, transportadoras, help desks, ferramentas de marketing, plataformas IoT, até apps internos de outro time. Fora pagamentos, você frequentemente perde garantias maduras de entrega, esquemas de evento estáveis e comportamento consistente de retry.
Os primeiros sinais geralmente são confusos:
- Eventos duplicados (a mesma atualização chega duas vezes)
- Eventos faltando (algo mudou, mas você nunca soube)
- Atrasos (uma atualização chega minutos ou horas depois)
- Eventos fora de ordem (um update "closed" chega antes do "opened")
Sistemas de terceiros instáveis fazem isso parecer aleatório porque falhas nem sempre são barulhentas. Um provedor pode dar timeout mas ainda assim processar sua requisição. Um balanceador pode cair uma conexão depois que o remetente já tentou novamente. Ou o sistema deles pode cair brevemente e enviar um pico de eventos antigos de uma vez.
Imagine um parceiro de entregas que envia webhooks "delivered". Um dia seu receptor está lento por 3 segundos, então eles retentam. Você recebe duas entregas, seu cliente recebe dois e-mails e o suporte fica confuso. No dia seguinte eles têm uma queda e nunca retentam, então "delivered" nunca chega e seu dashboard fica preso.
Confiabilidade de webhook é menos sobre uma requisição perfeita e mais sobre projetar para a realidade bagunçada: retries, idempotência e a habilidade de replay e verificar o que aconteceu depois.
Os três blocos: retries, idempotência, replay
Webhooks existem em duas direções. Webhooks inbound são chamadas que você recebe de outra parte (um provedor de pagamento, CRM, transportadora). Webhooks outbound são chamadas que você envia ao seu cliente ou parceiro quando algo muda no seu sistema. Ambos podem falhar por razões que não têm nada a ver com seu código.
Retries acontecem depois de uma falha. Um remetente pode re-tentar porque recebeu um timeout, um erro 500, uma conexão caída ou nenhuma resposta rápida o suficiente. Bons retries são comportamento esperado, não um caso raro. O objetivo é entregar o evento sem inundar o receptor ou criar efeitos colaterais duplicados.
Idempotência é como você torna duplicatas seguras. Significa "fazer uma vez, mesmo que recebido duas vezes". Se o mesmo webhook chegar novamente, você detecta e retorna sucesso sem executar a ação de negócio pela segunda vez (por exemplo, não criar uma segunda fatura).
Replay é seu botão de recuperação. É a habilidade de reprocessar eventos antigos de propósito, de maneira controlada, depois que você corrige um bug ou depois que um parceiro teve uma queda. Replay é diferente de retries: retries são automáticos e imediatos; replay é deliberado e muitas vezes acontece horas ou dias depois.
Se você quer confiabilidade de webhooks, defina algumas metas simples e projete ao redor delas:
- Sem eventos perdidos (você sempre pode achar o que chegou ou o que tentou enviar)
- Duplicatas seguras (retries e replays não cobram em dobro, não criam duplicatas, não enviam emails duplicados)
- Trilho de auditoria claro (você pode responder "o que aconteceu?" rapidamente)
Uma maneira prática de suportar os três é armazenar cada tentativa de webhook com um status e uma chave de idempotência única. Muitas equipes constroem isso como uma pequena tabela de "inbox/outbox" para webhooks.
Webhooks inbound: um fluxo receptor reutilizável
A maioria dos problemas de webhook acontece porque remetente e receptor estão em relógios diferentes. Seu papel como receptor é ser previsível: reconhecer rápido, registrar o que chegou e processar com segurança.
Separe "aceitar" de "fazer o trabalho"
Comece com um fluxo que mantém a requisição HTTP rápida e desloca o trabalho real para outro lugar. Isso reduz timeouts e torna retries muito menos dolorosos.
- Reconheça rapidamente. Retorne um 2xx assim que a requisição for aceitável.
- Cheque o básico. Valide content-type, campos obrigatórios e o parsing. Se o webhook for assinado, verifique a assinatura aqui.
- Persista o evento bruto. Armazene o body junto com os headers que vai precisar depois (assinatura, event ID), junto com timestamp de recebimento e um status como "received".
- Enfileire o trabalho. Crie um job para processamento em background e então retorne seu 2xx.
- Processe com resultados claros. Marque o evento como "processed" somente depois que os efeitos colaterais tiverem sucesso. Se falhar, registre por quê e se deve ser re-tentado.
Como é "responder rápido"
Uma meta realista é responder em menos de um segundo. Se o remetente espera um código específico, use-o (muitos aceitam 200, alguns preferem 202). Retorne 4xx apenas quando o remetente não deva tentar novamente (como assinatura inválida).
Exemplo: um webhook customer.created chega enquanto seu banco de dados está sob carga. Com esse fluxo, você ainda armazena o evento bruto, enfileira e responde 2xx. Seu worker pode re-tentar depois sem precisar que o remetente reenvie.
Checagens de segurança inbound que não quebram entrega
Checagens de segurança valem a pena, mas o objetivo é simples: bloquear tráfego ruim sem bloquear eventos reais. Muitos problemas de entrega vêm de receptores sendo rígidos demais ou retornando a resposta errada.
Comece provando o remetente. Prefira requests assinadas (header HMAC) ou um token compartilhado em um header. Verifique isso antes de fazer trabalho pesado e falhe rápido se estiver faltando ou incorreto.
Cuidado com códigos de status porque eles controlam retries:
- Retorne 401/403 para falhas de autenticação para que o remetente não tente para sempre.
- Retorne 400 para JSON malformado ou campos obrigatórios ausentes.
- Retorne 5xx apenas quando seu serviço estiver temporariamente incapaz de aceitar ou processar.
Listas de IP podem ajudar, mas somente quando o provedor tem ranges de IP estáveis e documentados. Se os IPs mudam frequentemente (ou usam um grande pool de cloud), allowlists podem dropar webhooks reais silenciosamente e você só nota muito depois.
Se o provedor inclui um timestamp e um ID único de evento, você pode adicionar proteção contra replay: rejeite mensagens muito antigas e rastreie IDs recentes para detectar duplicatas. Mantenha a janela de tempo pequena, mas permita uma margem para que deriva de relógio não quebre requisições válidas.
Uma checklist receptor-amigável de segurança:
- Valide assinatura ou segredo compartilhado antes de fazer parsing de payloads grandes.
- Aplique um tamanho máximo de body e um timeout de requisição curto.
- Use 401/403 para falhas de auth, 400 para JSON malformado e 2xx para eventos aceitos.
- Se checar timestamps, permita uma pequena janela de tolerância (por exemplo, alguns minutos).
Para log, mantenha um trilho de auditoria sem reter dados sensíveis indefinidamente. Armazene event ID, nome do remetente, tempo de recebimento, resultado da verificação e um hash do body bruto. Se precisar guardar payloads, defina retenção e mascarar campos como emails, tokens ou dados de pagamento.
Retries que ajudam, não atrapalham
Retries são bons quando transformam um pico breve em uma entrega bem-sucedida. São prejudiciais quando multiplicam tráfego, escondem bugs reais ou criam duplicatas. A diferença é ter uma regra clara do que re-tentar, como espaçar tentativas e quando parar.
Como base, re-tente apenas quando o receptor provavelmente terá sucesso depois. Um modelo mental útil é: re-tentar em falhas "temporárias", não re-tentar quando "você enviou algo errado".
Resultados HTTP práticos:
- Re-tentar: timeouts de rede, erros de conexão e HTTP 408, 429, 500, 502, 503, 504
- Não re-tentar: HTTP 400, 401, 403, 404, 422
- Depende: HTTP 409 (às vezes "duplicado", às vezes um conflito real)
Espaçamento importa. Use backoff exponencial com jitter para não criar uma tempestade de retries quando muitos eventos falham ao mesmo tempo. Por exemplo: espere 5s, 15s, 45s, 2m, 5m e adicione um pequeno offset aleatório a cada tentativa.
Também defina uma janela máxima de retry e um corte claro. Escolhas comuns são "tentar por até 24 horas" ou "não mais que 10 tentativas". Depois disso, trate como um problema de recuperação, não de entrega.
Para fazer isso funcionar no dia a dia, seu registro de evento deve captar:
- Contagem de tentativas
- Último erro
- Próxima hora de tentativa
- Status final (incluindo um estado de dead-letter quando parar de tentar)
Itens em dead-letter devem ser fáceis de inspecionar e seguros para reprocessar depois que você consertar o problema subjacente.
Padrões de idempotência que funcionam na prática
Idempotência significa que você pode processar o mesmo webhook mais de uma vez sem criar efeitos colaterais extras. É uma das maneiras mais rápidas de melhorar confiabilidade, porque timeouts e retries acontecem mesmo quando ninguém está errado.
Escolha uma chave que permaneça estável
Se o provedor fornece um event ID, use-o. Essa é a opção mais limpa.
Se não houver event ID, construa sua própria chave a partir de campos estáveis que você tenha, como um hash de:
- nome do provedor + tipo de evento + resource ID + timestamp, ou
- nome do provedor + message ID
Armazene a chave junto com um pequeno conjunto de metadados (tempo de recebimento, provedor, tipo de evento e o resultado).
Regras que normalmente funcionam:
- Trate a chave como obrigatória. Se não consegue construí-la, coloque o evento em quarentena em vez de chutar.
- Armazene chaves com TTL (por exemplo 7 a 30 dias) para que a tabela não cresça para sempre.
- Salve também o resultado do processamento (success, failed, ignored) para que duplicatas recebam uma resposta consistente.
- Coloque uma restrição única na chave para que duas requisições paralelas não rodem ambas.
Torne a ação de negócio idempotente também
Mesmo com uma boa tabela de chaves, suas operações reais precisam ser seguras. Exemplo: um webhook "create order" não deveria criar um segundo pedido se a primeira tentativa der timeout depois do insert no banco. Use identificadores de negócio naturais (external_order_id, external_user_id) e padrões de upsert.
Eventos fora de ordem são comuns. Se você receber "user_updated" antes de "user_created", decida uma regra como "aplicar mudanças apenas se event_version for mais novo" ou "atualizar apenas se updated_at for mais recente que o que temos".
Duplicatas com payloads diferentes são o caso mais difícil. Decida antecipadamente o que fará:
- Se a chave bate, mas o payload difere, trate como bug do provedor e alerte.
- Se a chave bate e o payload só difere em campos irrelevantes, ignore.
- Se não confiar no provedor, mude para uma chave derivada do hash completo do payload e trate conflitos como novos eventos.
O objetivo é simples: uma mudança no mundo real deve produzir um resultado real no mundo uma vez, mesmo se você vir a mensagem três vezes.
Ferramentas de replay e logs de auditoria para recuperação
Quando um sistema parceiro está instável, confiabilidade é menos sobre entrega perfeita e mais sobre recuperação rápida. Uma ferramenta de replay transforma "perdemos alguns eventos" em um conserto rotineiro em vez de uma crise.
Comece com um log de eventos que rastreie o ciclo de vida de cada webhook: received, processed, failed ou ignored. Mantenha pesquisável por tempo, tipo de evento e um correlation ID para que o suporte possa responder, "O que aconteceu com o pedido 18432?" rapidamente.
Para cada evento, armazene contexto suficiente para rodar a mesma decisão depois:
- Payload bruto e headers chave (assinatura, event ID, timestamp)
- Campos normalizados que você extraiu
- Resultado do processamento e mensagem de erro (se houver)
- Versão do workflow ou mapeamento usada na época
- Timestamps de receive, start, finish
Com isso pronto, adicione uma ação "Replay" para eventos falhos. O botão é menos importante que as proteções. Um bom fluxo de replay mostra o erro anterior, o que acontecerá no replay e se o evento é seguro de re-executar.
Guardrails que evitam danos acidentais:
- Exigir uma nota com o motivo antes do replay
- Restringir permissões de replay a um pequeno grupo
- Re-executar usando as mesmas checagens de idempotência da primeira tentativa
- Limitar por taxa os replays para evitar um novo pico durante incidentes
- Modo opcional dry run que valida sem escrever mudanças
Incidentes normalmente envolvem múltiplos eventos, então permita replay por intervalo de tempo (por exemplo, "replay de todos os eventos falhos entre 10:05 e 10:40"). Logue quem re-executou o quê, quando e por quê.
Webhooks outbound: um fluxo de envio que você pode auditar
Webhooks outbound falham por razões chatas: um receptor lento, uma queda breve, um problema de DNS ou um proxy que derruba requisições longas. Confiabilidade vem de tratar cada envio como um job rastreado e repetível, não como uma chamada HTTP avulsa.
Um fluxo de envio previsível
Dê a cada evento um ID único e estável. Esse ID deve permanecer o mesmo entre retries, replays e até reinícios de serviço. Se você gerar um novo ID por tentativa, dificulta deduplicação para o receptor e auditoria para você.
Em seguida, assine cada requisição e inclua um timestamp. O timestamp ajuda receptores a rejeitar pedidos muito antigos, e a assinatura prova que o payload não foi alterado em trânsito. Mantenha as regras de assinatura simples e consistentes para que parceiros as implementem sem adivinhação.
Rastreie entregas por endpoint, não apenas por evento. Se você enviar o mesmo evento para três clientes, cada destino precisa de seu próprio histórico de tentativas e status final.
Um fluxo prático que a maioria das equipes pode implementar:
- Crie um registro de evento com event ID, endpoint ID, hash do payload e status inicial.
- Envie a requisição HTTP com assinatura, timestamp e um header de idempotency key.
- Registre cada tentativa (hora de início, fim, status HTTP, mensagem de erro curta).
- Re-tente apenas em timeouts e respostas 5xx, usando backoff exponencial com jitter.
- Pare após um limite claro (máx. de tentativas ou idade máxima), então marque como falhado para revisão.
Esse header de chave de idempotência importa mesmo quando você é o remetente. Dá ao receptor uma maneira limpa de deduplicar se eles processaram a primeira requisição mas seu cliente não recebeu o 200.
Por fim, torne falhas visíveis. "Falhado" não deve significar "perdido". Deve significar "pausado com contexto suficiente para reprocessar com segurança".
Exemplo: parceiro instável e recuperação limpa
Seu app de suporte envia atualizações de ticket a um sistema parceiro para que os agentes vejam o mesmo status. Toda vez que um ticket muda (atribuído, prioridade atualizada, fechado), você posta um webhook ticket.updated.
Uma tarde o endpoint do parceiro começa a dar timeout. Sua primeira tentativa de entrega espera, atinge o timeout e você trata como "desconhecido" (pode ter chegado, pode não). Uma boa estratégia de retry então re-tenta com backoff em vez de disparar repetidas tentativas a cada segundo. O evento fica em fila com o mesmo event ID e cada tentativa é registrada.
Agora a parte dolorosa: se você não usa idempotência, o parceiro pode processar duplicatas. A tentativa #1 pode ter chegado a eles, mas a resposta nunca voltou. A tentativa #2 chega depois e cria uma segunda ação "Ticket closed", enviando dois emails ou criando duas entradas na timeline.
Com idempotência, cada entrega inclui uma chave derivada do evento (frequentemente o próprio event ID). O parceiro guarda essa chave por um período e responde "já processado" para repetições. Você para de adivinhar.
Quando o parceiro volta, replay é como você conserta a atualização realmente perdida (por exemplo, uma mudança de prioridade durante a queda). Você pega o evento do seu log de auditoria e faz replay uma vez, com o mesmo payload e idempotency key, assim é seguro mesmo que eles já tenham recebido.
Durante o incidente, seus logs devem contar a história claramente:
- Event ID, ticket ID, tipo de evento e versão do payload
- Número da tentativa, timestamps e próxima hora de retry
- Timeout vs resposta não-2xx vs sucesso
- Chave de idempotência enviada e se o parceiro reportou "duplicate"
- Um registro de replay mostrando quem re-executou e o resultado final
Erros comuns e armadilhas para evitar
A maioria dos incidentes de webhook não vem de um bug enorme. Vem de pequenas escolhas que silenciosamente quebram confiabilidade quando o tráfego aumenta ou um terceiro fica instável.
As armadilhas comuns em postmortems:
- Fazer trabalho lento dentro do handler HTTP (writes no DB, chamadas a APIs, uploads) até o remetente dar timeout e re-tentar
- Assumir que provedores nunca enviam duplicatas, então cobrar em dobro, criar pedidos duplicados ou enviar dois e-mails
- Retornar códigos errados (200 mesmo quando não aceitou o evento, ou 500 para dados inválidos que nunca vão suceder com retry)
- Enviar sem correlation ID, event ID ou request ID e depois gastar horas casando logs com relatórios de clientes
- Re-tentar para sempre, o que cria backlog e transforma a queda de um parceiro na sua própria queda
Uma regra simples se mantém: reconheça rápido, então processe com segurança. Valide só o necessário para decidir aceitar o evento, armazene-o e faça o resto assincronamente.
Códigos de status importam mais do que o pessoal espera:
- Use 2xx apenas quando você armazenou o evento (ou enfileirou) e confia que será tratado.
- Use 4xx para input inválido ou auth falha para que o remetente pare de tentar.
- Use 5xx apenas para problemas temporários do seu lado.
Defina um teto de retries. Pare após uma janela fixa (como 24 horas) ou número de tentativas, então marque o evento como "precisa de revisão" para que um humano decida o que reprocessar.
Checklist rápido e próximos passos
Confiabilidade de webhooks é principalmente sobre hábitos repetíveis: aceite rápido, dedupe agressivamente, re-tente com cuidado e mantenha um caminho de replay.
Verificações rápidas inbound (receptor)
- Retorne um 2xx rápido assim que a requisição estiver armazenada com segurança (faça trabalho lento de forma assíncrona).
- Armazene o suficiente do evento para provar o que você recebeu (e depurar depois).
- Exija uma chave de idempotência (ou derive uma de provedor + event ID) e aplique-a no banco.
- Use 4xx para assinatura inválida ou schema inválido, e 5xx apenas para problemas reais de servidor.
- Rastreie status de processamento (received, processed, failed) e a última mensagem de erro.
Verificações rápidas outbound (remetente)
- Atribua um event ID único por evento e mantenha-o estável entre tentativas.
- Assine todas as requisições e inclua um timestamp.
- Defina uma política de retry (backoff, máx de tentativas e quando parar) e siga-a.
- Rastreie estado por endpoint: último sucesso, última falha, falhas consecutivas, próxima tentativa.
- Logue cada tentativa com detalhe suficiente para suporte e auditoria.
Para ops, decida antecipadamente o que vai reprocessar (evento único, lote por intervalo de tempo/status, ou ambos), quem pode fazê-lo e como será a rotina de revisão de dead-letter.
Se quiser construir essas peças sem ligar tudo manualmente, uma plataforma no-code como AppMaster (appmaster.io) pode ser uma opção prática: você pode modelar tabelas de inbox/outbox de webhooks no PostgreSQL, implementar fluxos de retry e replay visualmente no Business Process Editor e lançar um painel administrativo interno para buscar e re-executar eventos falhos quando parceiros ficarem instáveis.
FAQ
Webhooks ficam entre sistemas que você não controla, então você herda timeouts, quedas, retries e mudanças de esquema desses provedores. Mesmo com seu código correto, você pode ver duplicatas, eventos faltando, atrasos e entregas fora de ordem.
Projete pensando em retries e duplicatas desde o início. Armazene cada evento recebido, responda com um 2xx rápido assim que ele estiver gravado com segurança, e processe de forma assíncrona usando uma chave de idempotência para que entregas repetidas não repitam efeitos colaterais.
Você deve reconhecer rapidamente após validação básica e armazenamento, geralmente em menos de um segundo. Se você fizer trabalho lento dentro do request, os remetentes dão timeout e tentam novamente, o que aumenta duplicatas e complica incidentes.
Trate idempotência como “faça a ação de negócio uma vez, mesmo se a mensagem chegar várias vezes.” Você a aplica usando uma chave de idempotência estável (frequentemente o event ID do provedor), gravando-a e retornando sucesso para duplicatas sem executar a ação novamente.
Use o event ID do provedor se existir. Caso contrário, derive uma chave a partir de campos estáveis que você confie, evitando campos que mudam entre tentativas. Se não for possível construir uma chave estável, coloque o evento em quarentena para revisão em vez de chutar um valor.
Retorne 4xx para problemas que o remetente não corrige com retry, como autenticação falha ou payload malformado. Use 5xx apenas para problemas temporários no seu lado. Seja consistente, já que o código de status controla frequentemente se o remetente tenta novamente.
Reitere em timeouts, erros de conexão e respostas temporárias do servidor como 408, 429 e 5xx. Use backoff exponencial com jitter e um limite claro — por exemplo, número máximo de tentativas ou tempo máximo — então mova o evento para um estado de “precisa de revisão”.
Replay é o reprocessamento deliberado de eventos passados após consertar um bug ou recuperar de uma queda. Retries são automáticos e imediatos. Um bom fluxo de replay precisa de um log de eventos, checagens idempotentes e proteções para evitar duplicar trabalho acidentalmente.
Presuma que eventos chegarão fora de ordem e defina uma regra que faça sentido para seu domínio. Uma abordagem comum é aplicar atualizações somente quando a versão do evento ou o timestamp for mais recente que o que você já tem, assim chegadas tardias não sobrescrevem um estado atual.
Monte uma tabela simples de inbox/outbox e uma pequena visão administrativa para buscar, inspecionar e re-executar eventos com falha. No AppMaster (appmaster.io) você pode modelar essas tabelas no PostgreSQL, implementar dedupe, retry e fluxos de replay no Business Process Editor e entregar um painel interno sem codificar todo o sistema.


