Depuração6 de agosto de 20268 min de leitura

Webhook do GitHub não chega: você tem uma tentativa e três dias

O GitHub não tenta de novo entregas de webhook com falha. Nem uma vez. O registro dura três dias, e depois disso um deploy ruim custou eventos para sempre.

TODOS OS OUTROS PROVEDORESentregafalhatentativatentativatentativaa rede tem várias chances de ser instávelGITHUBentregafalhanadasem segunda tentativa3 dias para perceber e reenviar à mão,depois o registro da entrega também some
Os outros grandes remetentes tratam uma falha como temporária. O GitHub a trata como definitiva e dá a você uma janela curta para perceber.

Seu CI não rodou. A notificação no Slack nunca foi publicada. Você abre a aba Recent Deliveries (entregas recentes) do GitHub e o evento está lá, com uma marca vermelha ao lado e um timestamp dentro dos noventa segundos em que seu app estava reiniciando.

Agora vem a parte que pega muita gente de surpresa. O GitHub não vai enviar de novo. Nem em cinco minutos, nem em uma hora, nem nunca. A documentação diz isso em uma frase, sem rodeios: "O GitHub não entrega novamente, de forma automática, as entregas com falha."

Se você chegou aos webhooks do GitHub vindo da Stripe ou da Shopify, isso é o oposto do que você se acostumou a esperar.

TL;DR

  • O GitHub dá ao seu endpoint 10 segundos para responder um 2XX
  • Não existe nova tentativa automática (retry). Uma entrega com falha é um evento perdido, a menos que um humano intervenha
  • Os registros de entrega duram 3 dias. Depois disso, você não consegue nem ver o que perdeu
  • X-GitHub-Delivery não muda no reenvio manual, então é a chave de idempotência correta
  • Como não há rede de segurança, a solução tem que ser "nunca falhar, para começo de conversa", o que significa persistir antes de processar

O que todos os outros fazem, e o que o GitHub faz

A Stripe tenta de novo com backoff exponencial por três dias. A Shopify tenta de novo oito vezes ao longo de quatro horas. O Slack tenta de novo três vezes. A Paddle faz até sessenta tentativas em cerca de dois dias. Cada um desses remetentes pressupõe que seu endpoint às vezes vai estar indisponível e se planeja para isso, e os números estão reunidos na nossa referência de provedores de webhook.

O GitHub envia a requisição. Se não receber um 2XX em até 10 segundos, registra a falha e segue em frente.

TODOS OS OUTROS PROVEDORESentregafalhatentativatentativatentativaa rede tem várias chances de ser instávelGITHUBentregafalhanadasem segunda tentativa3 dias para perceber e reenviar à mão,depois o registro da entrega também some

É uma decisão de design legítima, não um descuido. O GitHub entrega um volume enorme de eventos, a maioria informativa, e uma fila de novas tentativas nessa escala é cara. Mas isso joga todo o peso da confiabilidade em você, e em silêncio, porque nada no fluxo de configuração avisa que esse provedor se comporta de um jeito diferente dos outros.

A janela de três dias é a restrição real

As entregas com falha aparecem em Settings → Webhooks → Recent Deliveries, e você pode clicar em Redeliver (entregar novamente) em qualquer uma delas. Existe uma API REST para a mesma coisa, para webhooks de repositório, de organização e de GitHub App.

O limite: "Você pode reenviar as entregas de webhook que ocorreram nos últimos 3 dias."

Três dias parecem bastante até você comparar com o jeito como as equipes realmente percebem isso. Um webhook falha durante um deploy de sexta à noite. Ninguém olha a aba de entregas, porque nada disparou alerta. Na segunda de manhã, alguém pergunta por que um PR nunca recebeu label. Na terça, o registro da entrega já expirou, e você não consegue nem listar o que se perdeu, muito menos enviar de novo.

Compare com a Stripe, em que os eventos continuam disponíveis para consulta pela Events API por 30 dias, tenha a entrega dado certo ou não. No GitHub, o registro da entrega é a cópia. Quando ele expira, a única fonte da verdade que sobra é o estado do próprio repositório, e você fica reconstruindo "quais pushes aconteceram entre 18:40 e 18:42" a partir do log de commits.

Por que a entrega falhou

Siga esta lista na ordem.

O endpoint levou mais de 10 segundos

"Seu servidor deve responder com um 2XX em até 10 segundos após receber uma entrega de webhook."

Um evento push em um monorepo movimentado carrega um payload grande, e um handler que faz o parse dele, percorre a lista de commits e chama de volta a API do GitHub para buscar o conteúdo dos arquivos passa dos dez segundos sem fazer nada obviamente errado. Um bot que pede a um modelo para revisar o diff passa desse limite em todo push. O próprio GitHub recomenda uma fila: "talvez você queira configurar uma fila para processar os payloads de webhook de forma assíncrona." O verificador de timeout cronometra seu handler contra os dez segundos, e Seu handler de webhook chama um LLM mostra, em cada plataforma, a solução de responder primeiro.

A fila funciona. Responder antes mesmo de seu handler rodar também funciona, e é a mesma ideia, um salto mais cedo. Como receber webhooks no Cloudflare Workers mostra como o relay de webhooks deste post (um serviço que recebe, guarda e encaminha seus webhooks) faz isso: o 2XX sai da edge em cerca de um segundo, então os dez segundos são gastos na rede, e não percorrendo seus commits.

O segredo não confere

Se o webhook tem um segredo configurado, o GitHub assina com o cabeçalho X-Hub-Signature-256, e sua verificação precisa rodar sobre o corpo bruto. Serializar o JSON de novo antes de calcular o hash produz bytes diferentes e a verificação falha, o que responde um 4xx, que conta como entrega com falha, sem nova tentativa. Os detalhes de cada provedor estão em Como verificar assinaturas de webhook da Stripe, do GitHub e da Shopify.

Algo na frente do app recusou a requisição

Uma regra de WAF, um filtro de bots ou um middleware de autenticação que espera um cookie de sessão. Tráfego de webhook é tráfego POST anônimo vindo de uma faixa de IPs de datacenter, exatamente o perfil que a maioria das regras de proteção está ajustada para bloquear. A resposta que o GitHub registra vai ser um 401, 403 ou 429, e não algo vindo do seu código.

O webhook não está inscrito no tipo de evento

A entrega nem aparece, em vez de aparecer e falhar. Confira a lista de eventos do webhook.

É um GitHub App e a instalação perdeu o acesso

Os webhooks do app param para os repositórios de onde a instalação foi removida. A aba de entregas não mostra nada, o que parece idêntico a "o gatilho nunca disparou".

A chave de idempotência

O cabeçalho X-GitHub-Delivery é um GUID que identifica o evento. Duas propriedades fazem dele a chave certa:

Ele não muda no reenvio manual. A orientação do GitHub é explícita: "se você solicitar uma nova entrega, o cabeçalho X-GitHub-Delivery será o mesmo da entrega original", então, quando você clica em Redeliver depois de um deploy ruim, seu handler vê o mesmo ID que teria visto da primeira vez e consegue reconhecer o evento como um que já processou.

Ele é por evento, e não por payload, que é o que você quer. Dois pushes realmente distintos geram dois IDs, mesmo que os payloads sejam parecidos.

export async function POST(req: Request) {
  const raw = await req.text();
  if (!verifyGitHubSignature(raw, req.headers.get("x-hub-signature-256"))) {
    return new Response("unauthorized", { status: 401 });
  }

  const deliveryId = req.headers.get("x-github-delivery");
  const { rowCount } = await db.query(
    `INSERT INTO processed_events (event_id, provider)
     VALUES ($1, 'github') ON CONFLICT (event_id) DO NOTHING`,
    [deliveryId],
  );
  if (rowCount === 0) return new Response(null, { status: 200 });

  await queue.publish({ deliveryId, event: req.headers.get("x-github-event"), body: raw });
  return new Response(null, { status: 200 });
}

Responda 200 à duplicata, não um erro. O conjunto mais amplo de padrões, incluindo o que fazer quando a gravação do marcador e o efeito colateral não podem compartilhar uma transação, está em Você recebeu o mesmo webhook duas vezes.

Como projetar para um remetente que não tenta de novo

Com qualquer outro provedor dá para ser um pouco relaxado com disponibilidade, porque o cronograma de novas tentativas absorve as falhas. Aqui a restrição de design é outra: a única entrega garantida é a primeira, então a primeira tem que dar certo.

Isso significa que o handler faz o mínimo possível antes de responder 200. Verifique a assinatura, grave o corpo bruto em algum lugar durável, responda. Todo o resto, incluindo as partes com mais chance de ser lentas ou lançar erro, acontece depois da resposta e pode ser tentado de novo no seu próprio ritmo, sem o GitHub olhando.

Também significa que você precisa saber das falhas dentro da janela de três dias, e não quando alguém por acaso perceber. O GitHub não vai avisar. Não há e-mail de falha, nem notificação de endpoint desativado, nada equivalente ao aviso da Shopify ou ao aviso de desativação da Stripe. Se a única coisa capaz de revelar uma lacuna é um humano abrindo a aba de entregas, você não tem monitoramento, você tem sorte.

Uma checagem útil que não custa nada: crie um alerta para quando um repositório do qual você espera eventos regulares ficar em silêncio por mais tempo que o intervalo normal. Isso pega tanto "nosso endpoint está falhando" quanto "o webhook foi excluído numa limpeza de configurações", que é um jeito surpreendentemente comum de essa história terminar.

Onde o AnyHook entra

O problema estrutural é que a única cópia do evento no GitHub é um registro de entrega com pavio de três dias, e sua única cópia é o que seu handler conseguiu gravar antes de cair.

Aponte o webhook para in.anyhook.net/you/github e a cópia muda de lugar. O AnyHook responde 200 ao GitHub em cerca de um segundo, a partir de um worker na edge cuja única tarefa é verificar e persistir, então o prazo de 10 segundos nunca entra em jogo e um handler lento ou com falha não consegue mais colocar a entrega no estado de falha do qual o GitHub não se recupera.

A partir daí:

  • Tentamos de novo a entrega para seu próprio endpoint com backoff exponencial, que é a rede de segurança que o GitHub não oferece
  • Armazenamos o evento com cabeçalhos e corpo completos, com criptografia em repouso, e ele fica disponível para replay (um novo envio de um evento armazenado, feito por você) durante a janela do seu plano (3 dias no Free, 30 no Pro, 90 no Scale), em vez dos 3 dias do GitHub
  • O replay funciona por intervalo de tempo, então a janela do deploy de sexta vira uma seleção e um clique, e não uma reconstrução a partir do histórico de commits
  • Os alertas de falha disparam com 1, 5 e 20 falhas consecutivas, que é o sinal que o GitHub não manda

Seu handler ainda precisa ser idempotente, porque o reenvio manual e o replay existem. Nada elimina isso.

Conclusão

Todo hábito de depuração que você criou com a Stripe se transfere mal para cá. Não há nova tentativa para esperar, nem dead-letter queue (DLQ) para inspecionar, nem uma API de trinta dias para preencher a lacuna (backfill). Há uma tentativa, um registro de três dias dizendo se ela funcionou, e depois silêncio.

Responda 200 rápido, com o evento já gravado, deduplique por X-GitHub-Delivery e coloque um alerta para silêncios inesperados. Se hoje sua configuração não consegue recuperar uma entrega com falha, isso não é uma peculiaridade do GitHub que você está contornando, é o design que você aceitou.

Todos os posts6 de agosto de 2026 · 8 min

Pare de perder webhooks.

Troque uma URL. Tenha novas tentativas, um log de eventos e replay com um clique. Ou veja antes: crie uma conta, rode a simulação de queda de 30 segundos e veja um evento sobreviver ao seu servidor fora do ar. Não precisa de servidor.