Marcio Cunha

Idempotência e Retries em Webhooks: Como Lidar com Eventos Duplicados

Descubra como estruturar chaves de deduplicação e garantir entregas at-least-once em webhooks. Saiba o que fazer quando parceiros disparam o mesmo evento duas vezes.

Marcio Cunha•6 min
Também disponível em:EnglishEspañol
Resumo
  • Sistemas distribuídos operam com entregas at-least-once porque redes falham e reenvios automáticos acontecem o tempo todo
  • Chaves de deduplicação baseadas em identificadores únicos salvam o banco de dados de processar operações repetidas
  • Processos idempotentes geram o mesmo resultado prático mesmo quando executados múltiplas vezes com os mesmos dados
  • Janelas de retenção em cache distribuído evitam que requisições simultâneas burlem a checagem inicial
  • Respostas HTTP padronizadas garantem que o sistema parceiro entenda que o evento foi recebido sem precisar reenviar

A Realidade Caótica da Comunicação entre Sistemas

Imagine que você pediu uma pizza pelo aplicativo e, bem na hora de confirmar o pagamento, a internet caiu. O sistema não sabe se o pedido foi concluído ou se o dinheiro sumiu, então ele tenta enviar a instrução de novo. No mundo do desenvolvimento de software, chamamos essa incerteza de rede de entrega at-least-once, ou pelo menos uma vez. Na prática, isso significa que para garantir que nenhuma mensagem importante se perca no caminho, os servidores preferem enviar o mesmo aviso várias vezes a correr o risco de perder um único dado. Webhooks, que são notificações automáticas enviadas de um sistema para outro quando algo acontece, sofrem intensamente com esse comportamento.

Quando um parceiro de pagamento avisa que um boleto foi pago, ele dispara uma requisição HTTP para o seu servidor. Se o seu sistema demorar um segundo a mais para responder por causa de lentidão no banco de dados, o servidor do parceiro pensa que a mensagem não chegou. Imediatamente, ele dispara o mesmo aviso de novo. De repente, seu sistema recebe duas cópias idênticas do mesmo evento de pagamento em um intervalo de poucos milissegundos. Se você não estiver preparado, seu código pode processar o pagamento duas vezes, gerar duplicidade de extratos ou conceder créditos em dobro para o cliente, causando uma enorme dor de cabeça operacional.

O Conceito Vital de Idempotência

Para sobreviver a esse caos, precisamos adotar um conceito fundamental da engenharia chamado idempotência. Na matemática, uma operação é idempotente quando você pode aplicá-la várias vezes e o resultado final continua exatamente o mesmo da primeira execução. Pense no botão de um elevador: se você apertar o botão do quinto andar dez vezes seguidas, o elevador não vai subir para o quinquagésimo andar; ele simplesmente vai para o quinto e pronto. Apertar mil vezes produz o mesmo efeito prático de apertar uma única vez.

No desenvolvimento de APIs e webhooks, construir um endpoint idempotente significa ensinar seu sistema a reconhecer que uma requisição atual é exatamente igual a uma que já foi processada com sucesso no passado. Quando o segundo disparo chega, o sistema percebe que o trabalho já foi feito, descarta a duplicata e devolve uma resposta de sucesso como se estivesse tudo bem. Dessa forma, eliminamos o risco de efeitos colaterais indesejados, transformando um fluxo caótico de reenvios em um processo previsível, seguro e totalmente resiliente a falhas de rede.

Implementando Chaves de Deduplicação com Bancos de Dados

A ferramenta mais direta para combater mensagens duplicadas é a chave de deduplicação. Todo webhook sério costuma enviar junto com os dados um identificador único para aquele evento específico, muitas vezes chamado de ID de evento ou chave de idempotência. Quando o seu servidor recebe esse pacote, a primeira coisa que ele faz antes de mexer em qualquer regra de negócio é olhar para esse identificador e verificar se ele já existe em uma tabela de controle no banco de dados.

Para garantir que essa verificação seja à prova de falhas, utilizamos restrições de unicidade nas colunas do banco. Veja um exemplo prático em Node.js usando uma tabela relacional:

async function processarWebhook(evento) {
const { eventId, dados } = evento;

try {
// Tenta registrar o evento na tabela de controle
await db.query(
'INSERT INTO eventos_recebidos (event_id, status) VALUES ($1, $2)',
[eventId, 'PROCESSANDO']
);
} catch (erro) {
// Se houver violação de chave única, o evento já foi visto
if (erro.code === '23505') {
console.log(`Evento duplicado ignorado: ${eventId}`);
return { status: 200, mensagem: 'Já processado anteriormente' };
}++ throw erro;
}

// Executa a regra de negócio real com segurança
await executarRegraDeNegocio(dados);

await db.query(
'UPDATE eventos_recebidos SET status = $1 WHERE event_id = $2',
['CONCLUIDO', eventId]
);

return { status: 200, mensagem: 'Processado com sucesso' };
}

Nesse código, se duas requisições chegarem exatamente no mesmo segundo, o banco de dados rejeita a segunda inserção devido à restrição de unicidade na coluna event_id. Isso impede que ambos os processos executem a lógica de negócio ao mesmo tempo.

Lidando com Concorrência Extrema Usando Cache Distribuído

Embora a restrição de banco de dados resolva a maior parte dos problemas, sistemas de alta escala enfrentam um desafio extra chamado condição de corrida. Se dois disparos idênticos chegarem com nanossegundos de diferença e o banco de dados ainda estiver escrevendo o primeiro registro, ambos podem passar pela verificação inicial antes que o bloqueio seja efetivado. Para fechar essa brecha, arquiteturas modernas costumam utilizar sistemas de cache rápido em memória, como o Redis.

O Redis permite definir travas temporárias conhecidas como locks distribuídos ou chaves com tempo de expiração curto. Antes de consultar o banco de dados relacional, o servidor tenta gravar um registro com o ID do evento no cache usando um comando que falha se a chave já existir. Essa operação atômica em memória acontece de forma extremamente veloz, bloqueando qualquer tentativa duplicada antes mesmo que ela encoste no banco de dados principal, protegendo os recursos mais caros da sua infraestrutura contra picos de tráfego desnecessários.

O Papel dos Retries e Códigos de Status HTTP

Quando falamos de webhooks, o reenvio automático é gerado pelo remetente quando ele não recebe uma resposta clara e imediata. Por isso, a forma como seu servidor responde é crucial para evitar loops infinitos de reenvios. Se o seu código falhar por causa de um erro temporário, como uma oscilação na conexão com o banco de dados, você deve retornar um código de status HTTP na faixa de 500, como 503 Service Unavailable. Isso avisa o parceiro que o problema foi do seu lado e que ele deve tentar novamente mais tarde.

Por outro lado, quando um webhook é processado com sucesso ou quando ele é identificado como uma duplicata inofensiva, seu servidor deve retornar obrigatoriamente um código na faixa de 200, como 200 OK ou 204 No Content. Nunca retorne erros de cliente, como 400 Bad Request, para eventos duplicados válidos, a menos que o corpo da mensagem esteja corrompido de fato. Retornar um sucesso para uma duplicata faz com que o sistema parceiro entenda que a mensagem chegou ao destino final, encerrando o ciclo de reenvios e trazendo paz para os servidores de ambos os lados.

Conclusão

Construir integrações baseadas em webhooks exige abandonar a ilusão de que as redes de computadores são perfeitamente estáveis. Entender que duplicidades e atrasos fazem parte do dia a dia da engenharia nos obriga a projetar sistemas focados em resiliência, idempotência e controle rigoroso de concorrência. Ao combinar chaves de deduplicação inteligentes, restrições robustas em banco de dados e respostas HTTP adequadas, transformamos eventos caóticos em fluxos de dados previsíveis e seguros.

Em última análise, cuidar da idempotência não é apenas um detalhe técnico de programação, mas uma decisão de arquitetura que protege a integridade financeira e operacional do negócio. Quando seus sistemas conseguem absorver reenvios duplicados sem piscar, você ganha a tranquilidade necessária para escalar sua aplicação sem medo de surpresas desagradáveis nos registros dos clientes.