Como implementar idempotência em webhooks

A maioria dos provedores de webhooks opera com uma garantia de entrega "at least once". A expressão-chave aqui é "at least" — mais cedo ou mais tarde você vai receber o mesmo webhook várias vezes. A sua aplicação precisa ser construída para lidar com esses cenários e manter a integridade dos dados.

O que é idempotência?

Em computação, quando repetir a mesma ação produz o mesmo resultado, chamamos isso de idempotente. Um exemplo comum que você provavelmente já encontrou é a diferença entre os métodos HTTP PUT e POST.

A distinção entre os dois é que PUT indica que a ação é idempotente. Atualizar a contagem de um estoque, o primeiro nome de um perfil ou atribuir um pedido a um cliente pode ser feito várias vezes seguidas sem consumir recursos novos ou extras.

Um POST, por outro lado, implica efeitos colaterais. Se você cria um novo pedido, cada chamada ao endpoint criará uma nova entrada, mesmo que ela contenha as mesmas propriedades.

Como os webhooks são padronizados em torno de chamadas HTTP POST, cabe a você descobrir o que é idempotente por natureza e o que precisa ser construído para ser idempotente. Na maioria dos casos, o ônus recai sobre você.

Nunca perca um webhook.

O Hookdeck cuida de retries, deduplicação e recuperação de erros — para que entregas que falham não virem dados perdidos.

Quando construir para idempotência

De modo geral, eventos que criam um novo recurso ou causam efeitos colaterais em outros sistemas são os mais delicados. Você não iria querer criar o mesmo pedido várias vezes porque recebeu duas vezes o mesmo webhook da Shopify. Você também poderia causar efeitos colaterais, como enviar um e-mail quando um produto acaba no estoque, algo que ninguém quer fazer múltiplas vezes.

Esses são casos em que você precisaria auditar o seu código com cuidado, procurar por áreas onde problemas de idempotência possam surgir e então construir estratégias para tornar esses eventos de webhook idempotentes.

Estratégias de idempotência

Impor uma constraint de unicidade herdada dos dados do evento

Em muitos casos, você terá algum ID único que pode usar para saber se já executou a ação para determinada requisição de webhook. Por exemplo, se você está indexando pedidos de uma loja Shopify no tópico de webhook orders/created, pode usar o order_id da Shopify como propriedade única no seu banco de dados.

Em SQL, você poderia fazer algo assim:

 CREATE TABLE orders (
    id text PRIMARY KEY,
    shopify_order_id text UNIQUE NOT NULL,
		[...]
);

Essa é a solução mais simples se o seu banco de dados suporta constraints de unicidade. Se, digamos, você também quiser enviar um e-mail ao cliente, faça o INSERT antes de enviar o e-mail. Como você está usando essa constraint de unicidade para checar a idempotência, é melhor executar os efeitos colaterais depois. Por fim, garanta que você trate o erro e retorne um código 2XX status correspondente à violação da constraint de unicidade.

Rastrear o histórico e o status dos webhooks

Em alguns casos a primeira estratégia não estará disponível, talvez porque você não armazena nenhum desses dados. Ainda assim, todo provedor inclui algum identificador do próprio webhook. Na Shopify, a requisição contém X-Shopify-Webhook-Id nos headers. Você pode usar esse ID para rastrear o status dos webhooks que recebe.

Requisições repetidas do mesmo webhook terão o mesmo identificador.

Para lidar com esses cenários, você vai querer criar uma tabela processed_webhooks com uma constraint de unicidade no ID.

CREATE TABLE processed_webhooks (
    id text PRIMARY KEY,
		[...]
);

A primeira coisa a fazer quando você recebe a requisição é armazená-la na tabela usando o ID único do webhook. Assim que o seu método for concluído com sucesso, você pode atualizar a linha para o status COMPLETED. Caso não consiga processá-lo com sucesso, basta remover a linha e permitir novas tentativas.

Você pode envolver as chamadas de webhook em um método genérico para verificar a idempotência. Aqui vai um exemplo usando Postgres, Express e NodeJS:

const processWebhook = async (req, handler) => {
  // Extract the unique ID, using Shopify for this example
  const unique_id = req.headers["X-Shopify-Webhook-Id"];
  // Create a new entry for that webhook id
  await client
    .query("INSERT INTO processed_webhooks (id) VALUES $1", [unique_id])
    .catch((e) => {
      // PostgreSQL code for unique violation
      if (e.code == "23505") {
        // We are already processing or processed this webhook, return silently
        return true;
      }
      throw e;
    });
  try {
    // Call you method
    await handler(req.body);
    return true;
  } catch {
    // Delete the entry on error to make sure the next one isn't ignored
    await client.query("DELETE FROM processed_webhooks WHERE id = $1", [
      unique_id,
    ]);
  }
};

app.post("/webhooks/order-created", (req, res) => {
  // Wrap your doSomething method to handle your webhook
  return processWebhook(req, doSomething).catch(() => res.sendStatus(500));
});

Retries e idempotência

Retries automáticos são a principal fonte de entregas duplicadas de webhooks. Quando uma tentativa de entrega falha — por timeout, erro de servidor ou problema de rede — o provedor (ou a sua infraestrutura de webhooks) reenvia o mesmo evento. Sem handlers idempotentes, cada retry dispara a sua lógica de negócio de novo.

É por isso que a entrega at-least-once e a idempotência são conceitos inseparáveis. A entrega at-least-once garante que todo evento chegue até você, mas o preço são as possíveis duplicatas. A idempotência torna essas duplicatas seguras.

Pontos-chave para cenários de retry:

  • Timeouts são o modo de falha mais perigoso. Se o seu handler processa um evento com sucesso mas demora demais para responder, o provedor vê um timeout e reenvia. Você já processou o evento e um retry está a caminho. A sua checagem de idempotência é a única coisa impedindo efeitos colaterais duplicados.
  • Marque os eventos como processados antes de executar efeitos colaterais. Se você envia o e-mail primeiro e marca como processado depois, uma queda entre esses dois passos faz com que o retry envie o e-mail de novo. Inserir o registro de idempotência primeiro (dentro de uma transação, quando possível) evita isso.
  • Defina o TTL de idempotência para exceder a janela de retry. Se o seu provedor tenta novamente por 48 horas, o seu cache de deduplicação precisa persistir pelo menos esse tempo. Caso contrário, retries tardios vão passar.

Replay e idempotência

O replay de eventos — reentregar eventos deliberadamente para depuração ou recuperação — é uma das ferramentas mais poderosas de uma infraestrutura de webhooks. Depois de uma queda, uma correção de bug ou uma mudança de configuração, você pode reprocessar todos os eventos que falharam e se recuperar sem perda de dados.

O replay só é seguro quando os seus handlers são idempotentes. Sem idempotência, reprocessar eventos que foram parcialmente processados pode causar efeitos colaterais duplicados. Com idempotência, o replay é sempre seguro — os seus handlers detectam eventos já processados e os ignoram.

Implicações práticas:

  • Replay após uma correção de bug: você corrige um bug no handler e reprocessa os eventos que falharam. Alguns deles podem ter sido parcialmente processados antes de o bug causar a falha. Handlers idempotentes garantem que apenas as partes não processadas sejam concluídas.
  • Replay após uma queda: o seu serviço ficou fora do ar por uma hora. Os eventos enfileirados durante a queda são reprocessados. Alguns podem ter sido entregues, mas não reconhecidos. A idempotência evita o processamento em dobro.
  • Replay para depuração: você reprocessa um evento específico para reproduzir um problema em staging. A idempotência garante que não haja efeitos colaterais em produção se você reprocessar contra o ambiente errado por engano.

Estratégias de armazenamento para idempotency keys

O mecanismo de armazenamento das suas idempotency keys envolve trade-offs entre velocidade, durabilidade e complexidade operacional.

EstratégiaVelocidadeDurabilidadeSuporte a TTLMelhor para
Banco de dados (PostgreSQL, MySQL)ModeradaAlta (sobrevive a reinícios)Manual (job de limpeza)Cargas transacionais, operações financeiras
Redis / MemcachedRápidaSemidurável (persistência configurável)TTL nativoSistemas de alto throughput, maioria dos casos de webhooks
Em memória (Map, Set)A mais rápidaNenhuma (perdida no reinício)ManualApps de instância única, operações não críticas

Armazenamento em banco de dados é a escolha certa quando a idempotência precisa ser transacional — por exemplo, quando você precisa inserir o registro de idempotência e atualizar um saldo atomicamente, na mesma transação. O trade-off é a latência: uma consulta ao banco a cada webhook adiciona alguns milissegundos.

Armazenamento em Redis/cache é a escolha mais comum para idempotência de webhooks. É rápido o bastante para cenários de alto throughput, suporta TTL nativo para limpeza automática e oferece durabilidade por configuração de persistência (snapshots AOF ou RDB). Use Redis quando a sua checagem de idempotência não precisa estar na mesma transação que a sua lógica de negócio.

Armazenamento em memória só deve ser usado para operações não críticas, em que processar uma duplicata de vez em quando é aceitável. Um reinício do servidor limpa o cache, então qualquer evento retentado depois de um reinício será processado de novo. Isso está ok para invalidação de cache ou logging, mas não para transações financeiras.

Como o Hookdeck ajuda

Implementar corretamente o tratamento idempotente de webhooks é um daqueles problemas que parecem simples até você considerar retries, replays e trade-offs de armazenamento em cada fonte de eventos da sua stack. Construir isso do zero significa implementar geração de chaves únicas, armazenamento de deduplicação com o TTL correto, ordenação transacional dos efeitos colaterais e ferramental manual de replay (e então manter tudo isso ao lado do código do seu produto).

O Event Gateway do Hookdeck é infraestrutura gerenciada para receber e entregar webhooks de forma confiável. Ele suprime automaticamente entregas duplicadas antes que cheguem aos seus handlers, usando deduplicação baseada em conteúdo, agrupa falhas relacionadas em Issues para que você veja as causas raiz e retente em massa os eventos afetados com um clique, e reenvia entregas que falharam com backoff configurável, de modo que erros transitórios nunca produzam efeitos colaterais duplicados. Cada entrega é rastreada de ponta a ponta, com payload, headers e dados de resposta completos. Comece a usar o Hookdeck e adicione confiabilidade de produção ao seu pipeline de webhooks sem escrever você mesmo o código de enfileiramento.

Para saber mais sobre como as garantias de entrega interagem com a idempotência, veja Garantias de entrega de webhooks. Para uma abordagem abrangente de confiabilidade de webhooks, incluindo retries, replay e observabilidade, veja Assumindo o controle da confiabilidade dos seus webhooks.

FAQs

O que é idempotência em webhooks?

Idempotência em webhooks significa que processar o mesmo evento várias vezes produz o mesmo resultado que processá-lo uma única vez. Isso é essencial porque provedores de webhooks usam entrega at-least-once, ou seja, eventos duplicados são esperados. Handlers idempotentes evitam que duplicatas causem efeitos colaterais indesejados, como cobranças em dobro ou notificações repetidas.

Por que a idempotência é importante para webhooks?

Sem idempotência, entregas duplicadas de webhooks — causadas por retries, problemas de rede ou pelo comportamento do provedor — podem levar a cobranças em dobro, envio de e-mails duplicados ou corrupção de dados. A idempotência garante que, não importa quantas vezes um evento seja entregue, a sua aplicação o processe com segurança.

Como implemento processamento idempotente de webhooks?

Use um identificador único do evento (como um ID de pagamento, de pedido ou do próprio evento de webhook) para verificar se ele já foi processado. Armazene os IDs já processados em um banco de dados com constraint de unicidade ou em um cache como o Redis, e pule o processamento se o ID já existir.

O que acontece se eu não implementar idempotência?

Sem idempotência, cada entrega duplicada de webhook dispara a sua lógica de negócio novamente. Isso pode resultar em cobranças em dobro, baixas duplicadas de estoque, múltiplos e-mails de notificação, métricas infladas e dados corrompidos — qualquer um desses pode prejudicar a confiança dos clientes e a receita.

Como os retries afetam a idempotência?

Retries são a principal fonte de entregas duplicadas de webhooks. Quando uma entrega falha e é retentada, o seu handler recebe o mesmo evento de novo. Sem idempotência, o retry causa processamento duplicado. Com idempotência, o retry é detectado e ignorado com segurança.

Qual é a melhor estratégia de armazenamento para idempotency keys?

A melhor estratégia depende dos seus requisitos. Armazenar em banco de dados (PostgreSQL, MySQL) traz durabilidade e segurança transacional. O Redis oferece consultas rápidas com TTL configurável para limpeza automática. Armazenamento em memória é o mais rápido, mas volátil — adequado apenas para aplicações de instância única em que processar uma duplicata ocasionalmente é aceitável.