Pare de perder webhooks: construa uma porta de entrada durável

Por Diogo Hudson Dias
Engineer in a São Paulo office reviewing a dashboard of webhook deliveries with success and retry indicators on a large screen.

Seus webhooks de entrada estão falhando silenciosamente. Nem todos os provedores fazem retry de forma confiável, alguns exigem uma confirmação (ACK) em 3 segundos e outros mudam políticas sem aviso. Um evento perdido hoje vira uma bagunça de reconciliação amanhã: faturas não pagas, checks do GitHub órfãos ou um cliente cujo ticket de suporte nunca foi vinculado ao pagamento. Você só vai perceber quando o CFO ou o COO perceberem.

A verdade incômoda sobre webhooks de terceiros

A maioria das equipes de SaaS trata webhooks como notificações confiáveis. Não são. São mensagens best‑effort entregues por uma rede não confiável a um backend frequentemente frio. Algumas realidades para internalizar:

  • O comportamento de retry varia muito. A Stripe vai tentar a entrega por dias; o Slack espera um 2xx em 3 segundos e pode reenviar rapidamente se você não responder; alguns fluxos de dev (como túneis para testes locais do GitHub) não fazem retry nenhum. As políticas mudam e você não será avisado.
  • A entrega é at‑least‑once, não exactly‑once. Duplicatas e chegadas fora de ordem são normais. Se seus handlers não são idempotentes, você está montando uma bomba‑relógio.
  • Middleboxes podem quebrar assinaturas. CDNs e proxies que recodificam ou fragmentam (chunk) o corpo invalidam cálculos de HMAC. Se seu código de assinatura roda sobre o JSON já parseado, você já começou errado; ele precisa rodar sobre os bytes brutos exatos do corpo.
  • Cold starts e trabalho síncrono fazem você perder eventos. O Slack dá ~3 segundos. GitHub e Stripe esperam um 2xx rápido. Se você faz qualquer coisa não trivial antes de reconhecer, está criando suas próprias perdas.

Um relato recente de um desenvolvedor destacou uma dor comum: em desenvolvimento local, o GitHub não faz retry quando um túnel ou o laptop está fora do ar. Esse é um sintoma de dev de uma doença de nível de produção: se você não tornou a entrega durável do seu lado, está apostando o seu negócio na política de retry de terceiros.

A porta de entrada durável para webhooks

Você não precisa de Kafka, três novas equipes e um programa de seis meses. Você precisa de uma porta de entrada pequena e explícita cujo único trabalho é capturar, confirmar, verificar e enfileirar eventos com segurança. Depois, você pode processá‑los de forma assíncrona com SLOs adequados.

Restrições de design que você precisa cumprir

  • 2xx rápido: Responda em 50–200 ms em condições normais. O limite de 3 segundos do Slack é o mais rígido; use todo o seu orçamento apenas em uma gravação durável e nada mais.
  • Seguro para at‑least‑once: Todo componente após o ACK deve ser idempotente. Espere duplicatas, falta de ordenação e entregas parciais.
  • Verificação de assinatura byte‑exata: Calcule o HMAC sobre o corpo bruto da requisição, não sobre um objeto parseado. Se você faz deploy via um CDN ou gateway, garanta que ele preserve o stream bruto.
  • Isolamento do caos dos provedores: Trate as peculiaridades de cada provedor como políticas, não surpresas. Centralize tudo em um único lugar.

O blueprint

  1. Serviço de ingresso dedicado (fino, entediante, rápido): Termine o TLS, leia o stream bruto do corpo e grave — cabeçalhos e bytes — em um armazenamento de apêndice (append‑only). Não faça parse de JSON aqui. Não chame seu app. Não toque na lógica de negócio. Seu único trabalho é persistir e dar o ACK.
  2. Armazenamento durável de eventos brutos: Dois padrões práticos funcionam bem:
    • Tabela no Postgres particionada por dia com colunas: provider, path, method, headers (JSONB), raw_body (bytea), received_at (timestamptz), remote_addr, signature_status, checksum, delivery_id (if present). Índice em (provider, delivery_id) e received_at.
    • S3 + índice no Postgres: Armazene corpos brutos no S3 (compactados em gzip) e uma pequena linha de índice no Postgres com metadados e a chave do S3. Mais barato para corpos grandes, trivial de reter por 90+ dias.
  3. 2xx imediato após gravação durável: Assim que o evento bruto estiver com fsync concluído (ou o PUT no S3 retornar 200 e a linha do índice confirmar o commit), devolva 200 ou 202. Esse é o contrato.
  4. Worker de verificação de assinatura: Um worker simples consome novas linhas e executa a verificação específica de cada provedor usando os bytes brutos exatos. Marque eventos como verificados ou rejeitados. Eventos rejeitados nunca entram na fila de negócio, mas você ainda os mantém para forense e analytics de rate‑limit.
  5. Deduplicar e enfileirar: Derive uma chave de dedup. Prefira o ID de evento do provedor. Se não existir, faça o hash do tuplo (provider, path, subconjunto de headers canonicalizados, bytes brutos do corpo). Use uma janela de 30–90 dias. Publique um envelope pequeno e normalizado na sua fila de processamento (tópico no Kafka/Redpanda, SQS, ou uma tabela outbox no Postgres replicada para os workers).
  6. Handlers assíncronos (idempotentes, com retry): A lógica de negócio assina a fila. Todo handler deve ser idempotente e seguro para efeitos colaterais. Nenhum handler assume ordenação. Handlers emitem seus próprios checkpoints para permitir replays com segurança.
  7. Console de replay: Construa um UI mínimo para buscar eventos brutos, ver o estado de verificação e reenfileirar eventos selecionados. Você vai usar isso semanalmente.
  8. Jobs de backfill e reconciliação: Para provedores com APIs de listagem, agende varreduras periódicas para detectar desvios. Exemplo: pull noturno de eventos da Stripe por created timestamp e comparação com seu índice de dedup; buscar os delivery logs do GitHub Apps e cruzar IDs desconhecidos; para o Slack, comparar o histórico de canais com ações internas em fluxos críticos.

Números que mantêm você honesto

  • Orçamento de latência: Uma gravação durável no Postgres com synchronous_commit = on e NVMe local pode concluir em 3–12 ms no P50 e ficar abaixo de 50 ms no P95 sob carga moderada. PUT no S3 tipicamente retorna em 30–120 ms na mesma região. Combinado, você deve ficar muito abaixo do limite de 3 segundos do Slack com ampla folga.
  • Custo de armazenamento: 1 milhão de eventos/dia com corpo mediano de 1,2 KB dá aproximadamente 36 GB/mês de payload bruto. O S3 Standard custa poucos dólares para esse footprint; mesmo o dobro disso com metadados é irrelevante comparado ao risco de perder eventos.
  • Políticas de retry: A Stripe faz retry por até ~3 dias com backoff exponencial; o comportamento do Slack foca em redelivery rápido em segundos e espera ACKs ágeis; outros provedores oferecem redelivery best‑effort e replays manuais. Seu design não pode depender dessas políticas.

Segurança que você consegue provar

Segurança de webhook não é uma IP allowlist colada em um WAF. Faça o básico bem feito e você evita 90% dos incidentes:

  • Verificação de HMAC sobre o corpo bruto por provedor, com checagens de timestamp tolerantes à defasagem de relógio. Rejeite se os timestamps forem muito antigos ou muito à frente.
  • Rotação de segredos pelo menos trimestral. Guarde cópias criptografadas via KMS dos segredos atual e anterior para cobrir janelas de rotação.
  • Limites estritos de conteúdo: Limite o tamanho da requisição por provedor. Pré‑verifique a presença de cabeçalhos de assinatura antes de ler corpos grandes. Se as assinaturas estiverem ausentes, responda 400 cedo para evitar desperdício de ciclos.
  • Configuração de CDN/gateway: Se você precisar colocar um gateway na frente, use uma rota pass‑through. Desative transformações, compressão e qualquer middleware que possa mutar os bytes do payload. Preserve o stream da requisição como está.
  • Privilégio mínimo para processamento: O serviço de ingresso escreve no armazenamento bruto e em uma fila de verificação. Ele não pode tocar sistemas de negócio. Workers de verificação não podem mutar estado de negócio. Handlers com efeitos colaterais rodam com papéis rigidamente escopados.
  • Nunca registre segredos: Não registre payloads brutos em logs de app. Mantenha payloads apenas no armazenamento bruto, com criptografia em repouso e acesso estruturado e auditado.

Multi‑região sem dor de cabeça

Se você opera em duas regiões, dá para fazer a porta de entrada active‑active sem inventar um protocolo de consenso.

  • Load balancer global roteia para a região mais próxima. Espere duplicatas por reroutes transitórios e retries dos provedores; sua chave de dedup torna isso seguro.
  • Durabilidade local à região: Cada região grava no seu próprio armazenamento bruto. Replicação assíncrona (por exemplo, replicação entre regiões no S3 ou replicação lógica para o índice no Postgres) fornece uma visão unificada para seu console de replay.
  • Segredos consistentes: Mantenha segredos de provedores em um cofre replicado. Automatize a rotação em ambas as regiões.
  • Raio de impacto: Se uma região entrar em colapso, a outra continua aceitando e persistindo. Você pode processar a partir de uma única região até a recuperação; nada se perde.

SLOs operacionais e os únicos painéis que importam

A confiabilidade de webhooks morre nos vãos entre equipes. Publique SLOs e conecte‑os a alertas que seu on‑call realmente respeita:

  • SLO de ACK no ingresso: P99 do tempo até 2xx abaixo de 250 ms por provedor.
  • Fila de verificação: P95 abaixo de 60 segundos do recebimento até o estado verificado.
  • Fila de processamento: P95 abaixo de 5 minutos do verificado até o checkpoint do handler confirmado (commit) — mais apertado para ações visíveis ao usuário, como checks de CI ou comandos do Slack.
  • Sucesso de replay: 99% dos eventos brutos selecionados são reexecutados em até 2 minutos.
  • Taxa de desvio (drift): Menos de 0,01% de divergências entre APIs de listagem do provedor e seu índice de dedup na reconciliação noturna.

Uma única página deve mostrar: volume de entrada por provedor, latência de ACK, taxas de falha de verificação (com motivos: assinatura inválida, timestamp antigo, tamanho excedido), profundidade da fila, atraso dos handlers e uma métrica de drift verde/amarelo/vermelho das rotinas de reconciliação.

Armadilhas específicas de provedores para contornar

  • Slack: Janela de 3 segundos para ACK. Nunca chame seu app antes de responder. O Slack pode enviar redeliveries rápidos com cabeçalhos indicando a contagem de tentativas. Projete para isso.
  • Stripe: Expõe IDs de evento fortes; use‑os como chave de dedup. Sempre verifique via HMAC sobre o corpo bruto com o signing secret e tolerância de timestamp.
  • GitHub: As políticas evoluem. O GitHub suporta redelivery manual e retries best‑effort, mas você não deve depender disso. Para GitHub Apps, armazene o X‑GitHub‑Delivery ID para dedup e replay.
  • CDN e frameworks de servidor: Parsers de body em Node/Express, alguns API gateways e frameworks HTTP que fazem auto‑parse de JSON vão quebrar a verificação de HMAC se você não tomar cuidado. Capture o stream bruto antes de qualquer parser tocá‑lo.

Custo e complexidade: o que você consegue entregar em 4–6 semanas

Aqui vai um plano pragmático que já executamos com equipes enxutas:

  1. Semana 1–2: Levante o serviço de ingresso, o armazenamento bruto e o caminho de 2xx imediato. Ligue painéis básicos de latência de ACK e volume. Coloque um único provedor (geralmente Stripe ou Slack) atrás da nova porta de entrada.
  2. Semana 3: Adicione workers de verificação com HMAC sobre o corpo bruto. Implemente a tabela/índice de dedup e envelopes normalizados. Comece a alimentar uma fila (SQS ou Kafka) e porte um handler para processamento assíncrono.
  3. Semana 4: Construa o console mínimo de replay. Adicione alertas para falhas de verificação e SLOs de atraso. Job de backfill para o primeiro provedor.
  4. Semana 5–6: Migre os demais provedores. Adicione jobs de reconciliação quando houver APIs de listagem. Revise configurações de CDN/gateway para rotas pass‑through. Gire o primeiro lote de segredos de webhook para validar o caminho.

Depois disso, multi‑região vira exercício de configuração, não um redesenho. Você já tem dedup, replay e backfill. O resto é encanamento.

Por que isso importa ainda mais na era dos agentes

À medida que mais funcionalidades se tornam orientadas a eventos — automações de CI, reconciliações financeiras, agentes de IA reagindo a mudanças externas — a “verdade” do seu sistema depende de uma ingestão limpa e durável de sinais externos. Se você não consegue provar a entrega, não dá para confiar na automação. Webhooks duráveis são o substrato mínimo viável para agentes confiáveis e workflows determinísticos.

Trade‑offs e antipadrões

  • ACK após processar: Simples, porém fatal em escala. Sob cold starts ou picos, você vai perder eventos. Não faça.
  • Uma fila por provedor vs. um tópico compartilhado: Uma por provedor mantém o raio de impacto pequeno e o debug claro. Um tópico compartilhado pode funcionar se você impor esquemas estritos e chaves de roteamento.
  • Kafka em todo lugar vs. S3+Postgres primeiro: Se você já roda Kafka, ótimo. Se não, S3 + um índice no Postgres mais SQS é suficiente. Não deixe religião de infra atrasar a durabilidade.
  • Parse no ingresso: É tentador validar JSON imediatamente. Resista. Persista os bytes brutos primeiro para poder revalidar assinaturas para sempre, mesmo se bibliotecas ou codificações mudarem.
  • Confiar em dashboards de provedores: Ajudam, mas não são seu sistema de registro. Seu armazenamento bruto é.

Como é um bom resultado

Daqui a seis meses, você deve conseguir responder, com evidências, às perguntas que executivos e auditores realmente fazem:

  • Você consegue mostrar que todo evento da Stripe recebido nos últimos 90 dias foi persistido, verificado e ou processado ou rejeitado com motivo?
  • Quando o Slack teve um incidente regional na última terça, quantos eventos foram atrasados e quão rápido eles se normalizaram?
  • Quando você rotacionou os segredos de webhook do GitHub no último trimestre, alguma assinatura falhou por desvio ou mutação no gateway?
  • Seu on‑call consegue reexecutar o evento perdido de um cliente específico em menos de dois minutos, sem SSH ou scripts ad‑hoc?

Se você não consegue demonstrar isso hoje, está voando às cegas. A porta de entrada durável não é um nice‑to‑have; é uma superfície de controle do seu negócio.

Principais lições

  • Webhooks são mensagens best‑effort. Trate‑os como at‑least‑once e projete para duplicatas, desordem e peculiaridades de provedores.
  • Construa um ingresso fino que persista bytes brutos e dê ACK rápido. Verifique assinaturas e faça dedup de forma assíncrona antes da lógica de negócio.
  • Armazene todo evento em um store bruto, durável e consultável por 30–90 dias. É um seguro barato e sua fonte de verdade para replays.
  • Publique SLOs para latência de ACK, atraso de verificação, atraso de processamento e drift de reconciliação. Alerta neles.
  • Não dependa de retries ou dashboards de provedores. Assuma replay e backfill com seu console e seus jobs.

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation