Como serviços gerenciados de webhooks implementam entrega exactly-once
Se você já passou algum tempo construindo sistemas orientados a eventos, provavelmente esbarrou na promessa da entrega exactly-once de webhooks. Soa como o padrão-ouro: cada evento processado uma única vez, sem duplicatas e sem lacunas. Mas se você já tentou implementar isso, sabe que a realidade é mais confusa do que o marketing.
Em sistemas distribuídos, a entrega exactly-once é teoricamente impossível. O problema dos dois generais demonstra isso: duas partes se comunicando por um canal não confiável nunca podem ter certeza de que a outra recebeu a mensagem, porque o próprio reconhecimento pode se perder. Se um protocolo pudesse garantir entrega exactly-once, ele resolveria esse problema insolúvel.
Então o que os serviços gerenciados de webhooks realmente fazem quando afirmam oferecer semântica exactly-once? Eles não quebram as leis da computação distribuída. Em vez disso, empilham um conjunto de mecanismos em nível de infraestrutura (deduplicação, suporte a idempotência, controles de ordenação e retries inteligentes) sobre uma base de entrega at-least-once. O resultado é um sistema que, na prática, alcança processamento exactly-once, mesmo que a rede por baixo continue teimosamente não confiável.
Este artigo detalha como esses mecanismos funcionam, onde eles falham e o que você ainda precisa tratar no código da sua aplicação.
Por que a entrega exactly-once é impossível (e por que tudo bem)
Antes de entrar na implementação, ajuda entender por que a semântica exactly-once estrita está fora de alcance no nível da rede.
A entrega de um webhook é uma requisição HTTP de um remetente para um receptor. O remetente dispara a requisição e espera uma resposta. Três coisas podem acontecer:
- O receptor processa o evento e retorna 200. Todo mundo fica feliz.
- A requisição nunca chega. O remetente atinge o timeout e tenta de novo.
- O receptor processa o evento, mas a resposta 200 se perde no caminho. O remetente atinge o timeout e tenta de novo, e o receptor agora vê o que parece ser uma nova entrega do mesmo evento.
O cenário três é o cerne do problema. O remetente não tem como distinguir "o receptor nunca recebeu" de "o receptor recebeu, mas eu não recebi o reconhecimento". A única escolha segura é tentar de novo, o que significa que a entrega at-least-once é a garantia mais forte que se pode dar em uma rede não confiável.
Essa não é uma limitação específica de webhooks. O resultado de impossibilidade FLP (publicado por Fischer, Lynch e Patterson em 1985) provou que mesmo um único processo defeituoso torna o consenso distribuído impossível em um sistema assíncrono. Se você pudesse garantir a entrega exactly-once, poderia resolver o problema dos dois generais e, como ele é comprovadamente insolúvel, a entrega exactly-once também é.
A boa notícia: você não precisa de entrega exactly-once. Você precisa de processamento exactly-once. E esse é um problema de engenharia com soluções práticas.
At-least-once vs exactly-once: o que os termos realmente significam
A terminologia em torno da semântica de entrega de eventos pode confundir, porque sistemas diferentes usam as mesmas palavras com sentidos diferentes. Aqui vai uma taxonomia prática para garantias de entrega de webhooks:
| Garantia | O que significa | Compromisso |
|---|---|---|
| At-most-once | Dispara e esquece. Sem retries. Se a entrega falhar, o evento é perdido. | Simples, mas não confiável. Aceitável para telemetria não crítica. |
| At-least-once | O remetente persiste o evento e tenta de novo até receber um reconhecimento. O receptor pode ver duplicatas. | Confiável, mas exige que o receptor lide com duplicatas. |
| Processamento exactly-once | Entrega at-least-once combinada com consumidores idempotentes, que garantem que entregas duplicadas não produzam efeitos colaterais duplicados. | Confiável e seguro, mas exige cooperação entre infraestrutura e código da aplicação. |
Quando serviços gerenciados de webhooks falam em exactly-once, estão falando dessa terceira linha. A infraestrutura oferece entrega at-least-once com deduplicação e suporte a idempotência. A sua aplicação oferece handlers idempotentes. Juntos, vocês obtêm processamento exactly-once que, para todos os efeitos práticos, é o que você realmente quer.
Essa é a mesma abordagem usada pela semântica exactly-once do Apache Kafka, em que produtores idempotentes e escritas transacionais se combinam para evitar processamento duplicado entre brokers e consumidores. O Kafka consegue isso dentro de um sistema fechado que controla de ponta a ponta. Webhooks cruzam fronteiras de rede que você não controla, o que torna o problema mais difícil — e torna o suporte em nível de infraestrutura de um serviço gerenciado mais valioso.
Como os serviços gerenciados constroem o caminho até o processamento exactly-once
Um webhook gateway gerenciado como o Hookdeck Event Gateway fica entre os seus provedores de webhooks e a sua aplicação. Todo evento passa pela sua infraestrutura, o que significa que ele pode aplicar mecanismos de confiabilidade antes que o evento chegue ao seu endpoint. Veja como cada peça funciona.
Deduplicação de webhooks
A deduplicação de webhooks é a primeira linha de defesa contra processamento duplicado. A ideia é direta: se o serviço já viu um evento idêntico recentemente, ele descarta a duplicata antes de entregá-la.
O Hookdeck suporta três estratégias de deduplicação:
- Exact match: compara o payload inteiro. Se dois eventos forem idênticos byte a byte dentro da janela de deduplicação, o segundo é descartado. Isso é eficaz contra tempestades de retry, em que um remetente retransmite rapidamente o mesmo evento por causa de um timeout.
- Include fields: compara apenas campos específicos que você indica, como um ID de requisição ou uma referência de transação. Isso permite definir o que "duplicata" significa no seu domínio.
- Exclude fields: ignora campos indicados (como timestamps ou contadores de sequência que mudam a cada tentativa de entrega) e compara todo o resto.
Quando um evento chega, o Hookdeck calcula um hash com base na estratégia escolhida e o compara dentro de uma janela de tempo configurável, de 1 segundo a 1 hora. Se existir um hash correspondente nessa janela, o evento é suprimido.
Isso funciona, mas tem limites. Uma janela de deduplicação de uma hora não pega uma duplicata que chega duas horas depois. E se o provedor enviar dois eventos com payloads diferentes que representam a mesma operação lógica (por exemplo, dois eventos payment.updated com metadados ligeiramente diferentes), a deduplicação por campo pode não pegá-los. Deduplicação é uma otimização para reduzir carga, não uma garantia de correção. Ela diminui as duplicatas que a sua aplicação precisa tratar, mas não as elimina por completo.
Idempotência de webhooks
A idempotência de webhooks é onde a correção mora. Uma operação idempotente produz o mesmo resultado independentemente de quantas vezes você a executa. Se o seu handler de webhook é idempotente, receber o mesmo evento três vezes tem o mesmo efeito que recebê-lo uma vez.
Serviços gerenciados suportam idempotência de duas formas.
Idempotency keys fornecidas pela infraestrutura. Todo evento que o Hookdeck entrega inclui um header X-Hookdeck-EventID, um identificador único e estável daquele evento. Mesmo que as condições de rede façam o Hookdeck entregar o mesmo evento duas vezes, o seu handler pode usar esse ID para detectar a duplicata. O padrão é simples:
- Extraia o ID do evento do header.
- Verifique no seu banco de dados se existe um registro desse ID.
- Se existir, pule o processamento e retorne 200.
- Se não existir, processe o evento e grave o ID.
Essa é a mesma abordagem que a Stripe recomenda para os eventos de webhook dela. Todo evento da Stripe tem um campo id único, e a documentação afirma explicitamente que endpoints podem receber o mesmo evento mais de uma vez. O consenso da indústria é claro: processamento idempotente de webhooks é responsabilidade do receptor, e o papel da infraestrutura é dar as ferramentas para você fazer isso de forma eficiente.
Imposição no nível do banco de dados. Uma implementação comum usa uma constraint de banco para garantir unicidade:
CREATE TABLE processed_events (
idempotency_key TEXT PRIMARY KEY,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Tente inserir o ID do evento. Se a inserção funcionar, o evento é novo. Se violar a constraint de unicidade, é uma duplicata. Esse padrão é simples, testado em produção e funciona com qualquer banco relacional.
Para sistemas de alto throughput, você pode usar um set do Redis com expiração por TTL em vez de uma tabela. O compromisso é a durabilidade: se o Redis reiniciar, você perde o estado de deduplicação. Para a maioria dos volumes de webhooks, uma tabela com limpeza periódica das entradas antigas (qualquer coisa mais velha que a sua janela máxima de retry) é a escolha pragmática.
Retries de webhooks
Os retries são o que torna a entrega at-least-once possível. Se o receptor não confirma a entrega com um código de status de sucesso, o remetente tenta de novo.
A sofisticação está em como você faz o retry. Uma lógica ingênua (simplesmente reenviar a cada 5 segundos) cria problemas em escala. Se um destination cai, todos os eventos pendentes começam a tentar de novo ao mesmo tempo, criando uma manada que pode manter o destination fora do ar mesmo depois que ele se recupera.
O Hookdeck oferece estratégias de retry configuráveis:
- Backoff exponencial: cada tentativa espera o dobro da anterior. A primeira pode acontecer após 30 segundos, a próxima após 1 minuto, depois 2 minutos, e assim por diante. Isso dá fôlego para endpoints em dificuldade se recuperarem.
- Intervalos lineares: novas tentativas em intervalos fixos. Útil quando você sabe que o tempo de recuperação do seu destination é previsível.
- Agendas customizadas: você define os horários exatos das tentativas. Útil para alinhar retries com janelas de manutenção conhecidas.
Além disso, você pode adicionar jitter (variação aleatória somada aos intervalos de retry), que evita tempestades sincronizadas de retry quando muitos eventos estão falhando ao mesmo tempo. As janelas de retry podem se estender por até uma semana, com até 50 tentativas de entrega por evento.
Mas os retries interagem com a idempotência e a deduplicação de formas que importam. Um retry é, por definição, uma entrega duplicada. Se a sua janela de deduplicação for menor que a sua janela de retry, retries tardios passarão pela deduplicação e chegarão à sua aplicação. Isso é intencional: a camada de deduplicação trata o caso comum (retransmissões rápidas), e o seu handler idempotente pega todo o resto.
Ordenação de webhooks
A ordenação de webhooks é onde as coisas ficam particularmente delicadas na entrega de webhooks em sistemas distribuídos. Em teoria, se um evento customer.created é seguido por um customer.updated, você gostaria de processá-los nessa ordem. Na prática, não há garantia de que eles chegarão assim.
Webhooks são normalmente entregues por HTTP, que é sem estado e sem conexão. Um provedor pode enviar dois eventos a partir de servidores diferentes, ou o caminho de rede do primeiro evento pode ser mais lento que o do segundo. Não dá para garantir a ordem em que você recebe os webhooks.
Serviços gerenciados oferecem alguns mecanismos para ajudar:
Controle de concorrência. O Hookdeck permite definir uma taxa de entrega por destination, incluindo um limite de concorrência. Definir a concorrência como 1 dá entrega serial: os eventos são entregues um a um, na ordem em que o Hookdeck os recebeu. Isso não garante que eles chegaram ao Hookdeck em ordem, mas evita o reordenamento que a entrega paralela introduz.
Resolução de conflitos por timestamp. A maioria dos payloads de webhook inclui um timestamp. O seu handler pode comparar o timestamp do evento com o do último evento processado para aquele recurso. Se o evento que chegou for mais antigo, ignore. Se for mais novo, processe. Isso garante que o seu sistema sempre reflita o estado mais recente, mesmo quando os eventos chegam fora de ordem.
Padrão fetch-before-process. Em vez de confiar no payload do webhook, use-o como um sinal para buscar o estado atual na API do provedor. Isso contorna a ordenação por completo: você sempre obtém os dados mais recentes, independentemente de qual evento disparou a busca. A desvantagem é a latência adicional e o consumo de rate limit da API, mas para dados críticos é a abordagem mais robusta.
A tensão aqui é real: ordenação estrita exige processamento serial, o que limita o throughput. Na maioria dos sistemas, a resposta certa é projetar os handlers para tolerar entrega fora de ordem, em vez de forçar a infraestrutura a evitá-la.
Modos de falha de webhooks que quebram as garantias
Mesmo com todos esses mecanismos no lugar, os modos de falha de webhooks podem minar as suas garantias de entrega. Entender onde as coisas dão errado ajuda a projetar para resiliência em vez de torcer pela perfeição.
Indisponibilidade do destination
Quando o seu endpoint cai, os eventos se acumulam. Um serviço gerenciado como o Hookdeck detecta a backpressure (o atraso crescente entre a chegada e a entrega do evento) e pode abrir uma issue para notificar o seu time. Mas, em indisponibilidades prolongadas, a janela de retry dos primeiros eventos pode expirar antes que o destination se recupere.
O padrão circuit breaker funciona como válvula de segurança aqui. Quando um destination falha de forma consistente, o circuit breaker para de enviar requisições, evitando retries desperdiçados e dando tempo para o destination se recuperar. Assim que ele volta a responder, o circuito passa para o estado meio aberto, retomando a entrega com cautela antes de liberar o fluxo por completo.
Eventos venenosos
Alguns eventos nunca serão processados com sucesso: talvez o payload referencie um recurso que não existe no seu sistema, ou dispare um caminho de código com um caso de borda não tratado. Sem intervenção, esses eventos são retentados indefinidamente, consumindo recursos e entupindo o pipeline.
A solução é uma dead letter queue. Depois de um número configurado de tentativas frustradas, o evento é movido para uma fila separada, onde pode ser inspecionado, depurado e então corrigido e reprocessado ou descartado. O sistema de gestão e retry de eventos do Hookdeck dá visibilidade sobre entregas que falharam e a possibilidade de retentar ou descartar eventos manualmente, cumprindo um papel semelhante.
Deduplicação em split-brain
Se o estado da sua deduplicação vive em um único nó e esse nó fica indisponível, você perde a deduplicação temporariamente. Eventos que teriam sido suprimidos passam. Esse é um problema geral de qualquer camada de deduplicação com estado, gerenciada ou auto-hospedada. A mitigação é a idempotência em nível de aplicação, a sua última linha de defesa, que funciona independentemente do estado da infraestrutura.
Duplicatas geradas pelo provedor
Às vezes a duplicação acontece antes de os eventos chegarem ao serviço gerenciado. Um provedor pode gerar dois eventos distintos para a mesma operação lógica por causa de retries internos ou de consistência eventual nos próprios sistemas. Esses eventos terão IDs diferentes, então nem a deduplicação em nível de infraestrutura nem a idempotência baseada no ID do evento vão pegá-los. O seu handler precisa de idempotência em nível de domínio: verificar, por exemplo, se um pagamento já não foi registrado para uma determinada fatura, independentemente de qual evento o disparou.
Partições de rede
Em um cenário de partição de rede, os nós do pipeline de entrega não conseguem se comunicar entre si. O serviço gerenciado pode acreditar que um evento não foi entregue (porque nunca recebeu o reconhecimento), enquanto o destination na verdade o processou. O serviço tenta de novo e a sua aplicação vê uma duplicata. De novo, handlers idempotentes são a resposta.
O modelo de defesa em camadas
Se você percebeu um tema, é este: a confiabilidade de webhooks vem de camadas, não de um único mecanismo. Veja como as camadas se empilham:
| Camada | Mecanismo | O que ela pega | O que ela deixa passar |
|---|---|---|---|
| Deduplicação na infraestrutura | Comparação por hash dentro de uma janela de tempo | Tempestades de retry, retransmissões rápidas | Duplicatas tardias, eventos semanticamente equivalentes com payloads diferentes |
| Idempotency keys | ID único do evento nos headers | Qualquer reentrega exata do mesmo evento | Duplicatas geradas pelo provedor, com IDs diferentes |
| Idempotência em nível de aplicação | Verificações específicas do domínio (ex.: "esta fatura já foi paga?") | Tudo, inclusive duplicatas do provedor | Nada — esta é a última linha de defesa |
| Controles de ordenação | Limites de concorrência, comparação de timestamps | Processamento fora de ordem | Eventos que já chegam fora de ordem do provedor à infraestrutura |
| Lógica de retry | Backoff exponencial com jitter | Falhas transitórias, quedas breves | Quedas prolongadas além da janela de retry, eventos venenosos |
| Circuit breakers e DLQs | Detecção de falhas e isolamento de eventos | Falhas sustentadas do destination, eventos venenosos | Falhas intermitentes que não acionam o breaker |
Nenhuma camada isolada é suficiente. A deduplicação na infraestrutura reduz a carga sobre a sua aplicação. As idempotency keys pegam o que a deduplicação deixa passar. A idempotência em nível de aplicação pega todo o resto. É por isso que a distinção entre entrega at-least-once e exactly-once importa menos, na prática, do que ter construído todas as camadas.
O que isso significa para a sua arquitetura
Se você está avaliando infraestrutura de webhooks (construir por conta própria ou usar um serviço gerenciado), aqui estão as conclusões práticas:
Aceite a entrega at-least-once como base. Toda implementação séria de entrega de webhooks em sistemas distribuídos começa por aí. Tentar construir exactly-once de verdade no nível da rede é perda de tempo. Construa sobre at-least-once e invista nas camadas acima.
Implemente handlers idempotentes independentemente do que a sua infraestrutura oferece. Mesmo que o seu serviço gerenciado deduplique 99% das duplicatas, você precisa tratar o 1% restante. Use os IDs dos eventos como idempotency keys, armazene-os no seu banco e projete as mutações de estado para serem seguras quando aplicadas várias vezes.
Use a deduplicação da infraestrutura para proteger a sua aplicação, não para substituir a idempotência. Uma janela de deduplicação de 5 minutos pega a maioria das tempestades de retry e reentregas acidentais. Trate isso como uma otimização de desempenho que reduz a carga sobre os seus handlers idempotentes, não como um mecanismo de correção.
Projete para entrega fora de ordem. A menos que você precise mesmo de processamento serial (e esteja disposto a pagar o custo em throughput), construa handlers que comparem timestamps ou busquem o estado atual na API de origem. Assuma que os eventos podem chegar em qualquer ordem e projete de acordo.
Prefira upserts a inserts. Quando um handler de webhook atualiza o seu banco, use INSERT ... ON CONFLICT UPDATE em vez de uma lógica separada de "verificar e então inserir". Upserts são naturalmente idempotentes e lidam bem tanto com eventos novos quanto com duplicatas.
Monitore o seu pipeline de entrega. Serviços gerenciados fornecem métricas de taxa de entrega, taxa de erro, tamanho de fila e latência de resposta. Use-as. Uma fila crescendo ou uma taxa de erro subindo é um aviso antecipado de que as suas garantias de processamento exactly-once estão sob estresse.
Conclusão
A entrega exactly-once de webhooks como garantia em nível de rede não existe. A física dos sistemas distribuídos não permite. Mas o processamento exactly-once (garantir que cada evento tenha o efeito pretendido uma única vez) é alcançável com a combinação certa de salvaguardas na infraestrutura e na aplicação.
Serviços gerenciados de webhooks como o Hookdeck não afirmam quebrar as leis da computação distribuída. O que eles fazem é assumir o trabalho pesado de persistência, retries, deduplicação e gestão de entrega, para que você possa focar na única coisa que só a sua aplicação pode fazer: processar eventos de forma idempotente. A infraestrutura reduz as duplicatas que o seu código precisa tratar de "muitas" para "quase nenhuma". Os seus handlers idempotentes pegam o resto.
Essa é a resposta honesta sobre o que exactly-once significa na prática. Não uma garantia mágica, mas uma pilha bem projetada de mecanismos que, juntos, entregam as garantias de entrega de webhooks de que o seu sistema precisa.
Infraestrutura de webhooks, gerenciada para você
A Hookdeck cuida da ingestão, entrega, observabilidade e recuperação de erros — para que você não precise.