Webhooks14 de outubro de 20269 min de leitura

Seu handler de webhook chama um LLM. O remetente desiste antes.

O Slack espera 3 s, a Shopify 5 s, o GitHub 10 s. Um LLM leva mais: o remetente desiste. Responda antes e trabalhe depois, ou use um relay.

NO HANDLER · O REMETENTE ESPERA SEU MODELOSlackverificação + chamada ao LLM8 so Slack desiste em 3 stenta de novo → duplicaATRÁS DE UM RELAY · O RELÓGIO DO REMETENTE PARA ALISlackAnyHookarmazena, depois 200filanovas tentativashandlerchamada LLM, até 60 so remetente recebe o 200 em cerca de um segundo; seu modelo ganha o tempo de que precisa
Dentro do handler, o remetente espera a chamada ao seu modelo. Atrás de um relay, não espera nada além do relay.

Os remetentes de webhooks esperam segundos, não minutos: o Slack dá 3 segundos para seu handler responder, a Shopify 5, o GitHub 10, e um handler que chama um LLM costuma precisar de mais tempo; então o remetente registra uma falha e tenta de novo ou desiste; se isso se repetir o bastante, o Slack desativa suas inscrições de eventos e a Shopify pode excluir o webhook. A solução é responder 200 assim que a requisição for verificada e fazer a chamada ao modelo depois da resposta, seja num trabalho em segundo plano que você mesmo executa, seja atrás de um relay de webhooks (um serviço que recebe, guarda e encaminha seus webhooks) que responde ao remetente por você.

Todo o resto deste texto é a versão longa dessas duas frases: quanto tempo cada remetente espera de fato, o que ele faz quando você perde o prazo, quatro formas curtas de responder primeiro e trabalhar depois (com o porém de cada uma) e o que muda quando, em vez disso, um relay guarda o evento.

TL;DR

  • O relógio do remetente cobre o handler inteiro: verificação de assinatura, leituras no banco de dados e a chamada ao modelo
  • Perca o prazo e o remetente tenta de novo. Um handler que já fez o trabalho faz tudo outra vez: duas respostas no Slack, um pedido marcado duas vezes, dois e-mails
  • Perca o prazo com frequência suficiente e o Slack desativa suas inscrições de eventos, a Shopify pode excluir o webhook. O GitHub nunca tenta de novo
  • Responda primeiro, trabalhe depois: after() no Next.js, ctx.waitUntil() na Cloudflare (30 segundos no máximo), uma tarefa em segundo plano no Express ou no FastAPI
  • Nenhuma dessas formas guarda o evento se o trabalho em segundo plano morrer. Uma versão durável precisa de armazenamento, de um worker e de um cronograma de novas tentativas (retries), ou seja, de uma fila
  • Um relay é a fila que você não precisa escrever: o AnyHook responde ao remetente em cerca de um segundo e dá 60 segundos ao seu handler no Free, dentro da franquia de computação do plano, com novas tentativas

Quanto tempo cada remetente espera

Esses números vêm da documentação de cada provedor e estão reunidos, com as fontes, na nossa referência de provedores de webhook.

RemetenteEspera pelo seu 2xxDepois de uma entrega com falha
Slack3 segundos3 novas tentativas: imediatamente, após 1 min, após 5 min. Inscrições de eventos desativadas se mais de 95% das tentativas falharem em 60 min
LINE2 segundos (uma resposta mais tarde é registrada como request_timeout)O reenvio vem desligado por padrão. Pode suspender o envio de webhooks se seu servidor continuar falhando
Shopify5 segundos (1 segundo para conectar)8 novas tentativas em 4 horas. Uma inscrição criada pela Admin API é excluída após 8 falhas consecutivas
HubSpot5 segundos para o lote inteiro, de até 100 notificaçõesAté 10 tentativas distribuídas ao longo de 24 horas
Paddle5 segundosEm produção: até 60 tentativas em cerca de 2 dias
GitHub10 segundosNenhuma. Você pode reenviar manualmente por 3 dias
Twilio15 segundos por padrão, configurável1 nova tentativa por padrão, configurável de 0 a 5
StripeNão publicado. A documentação manda responder 2xx antes de qualquer lógica lentaBackoff exponencial por até 3 dias no modo de produção

Seu prazo real é o do remetente mais apertado de quem você recebe webhooks. Um bot no Slack tem 3 segundos, e um cold start pode gastar a maior parte disso antes de a primeira linha do seu código rodar.

Por que uma chamada a um LLM estoura o prazo

A chamada ao modelo é a etapa lenta de quase todo handler escrito em 2026. Uma completion curta leva segundos; uma com chamada de ferramenta, resposta longa ou nova tentativa dentro do seu SDK leva dezenas de segundos. Some um cold start e o remetente já desistiu antes de chegar o primeiro token.

O que acontece depois depende do remetente, e nada disso é bom:

  • O Slack tenta de novo. Seu handler recebe o mesmo evento outra vez enquanto a primeira execução ainda espera o modelo. As duas execuções terminam e as duas postam. Os usuários veem o bot responder duas vezes
  • A Shopify tenta de novo e depois exclui. Oito falhas consecutivas em quatro horas e uma inscrição criada pela Admin API desaparece. Os pedidos continuam chegando e seu app deixa de saber deles
  • O GitHub não faz nada. A entrega fica marcada como falha e a história acaba aí, a menos que alguém a reenvie manualmente em até três dias
  • O LINE deixa o reenvio desligado, a menos que você o tenha ativado; então um timeout é uma mensagem que seu bot nunca respondeu

O handler não falhou. Ele foi mais lento que um relógio que não conseguia ver.

A solução: responda primeiro, trabalhe depois

Verifique a requisição, responda 200 e só então chame o modelo. Toda plataforma tem um jeito de continuar rodando depois da resposta. Cada uma também tem um porém, descrito abaixo do código.

Next.js (App Router)

// app/api/slack/events/route.ts
import { after } from "next/server";

export async function POST(req: Request) {
  const raw = await req.text();
  if (!verifySlackSignature(raw, req.headers)) {
    return new Response("bad signature", { status: 401 });
  }
  const body = JSON.parse(raw);
  if (body.type === "url_verification") {
    return Response.json({ challenge: body.challenge });
  }

  after(async () => {
    const answer = await askModel(body.event.text); // the slow part
    await postToSlack(body.event.channel, answer);
  });

  return new Response(null, { status: 200 }); // well inside 3 seconds
}

O porém: a função after() roda pelo tempo máximo da rota e nada além disso. Se a chamada ao modelo lançar uma exceção, ou se a instância for reciclada, o evento se perde. O Slack já recebeu o 200 e não vai enviá-lo de novo, e nada registrou que ele existiu.

Cloudflare Workers

export default {
  async fetch(req: Request, env: Env, ctx: ExecutionContext) {
    const raw = await req.text();
    if (!(await verify(raw, req.headers, env))) {
      return new Response("bad signature", { status: 401 });
    }
    ctx.waitUntil(handle(JSON.parse(raw), env)); // runs after the response
    return new Response(null, { status: 200 });
  },
};

O porém: o método waitUntil tem no máximo 30 segundos depois da resposta, divididos entre tudo o que você passou para ele, e depois é cancelado. Uma chamada lenta ao modelo não cabe nisso. A resposta da própria Cloudflare para trabalhos mais longos é uma Queue com um Worker consumidor, e é a resposta certa. Nosso guia de Workers cobre as outras armadilhas da plataforma.

Express ou qualquer servidor Node de longa duração

app.post("/webhooks/slack", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifySlackSignature(req.body, req.headers)) return res.sendStatus(401);
  res.sendStatus(200); // answer first
  setImmediate(() => handle(JSON.parse(req.body)).catch(console.error));
});

O porém: isso funciona até o processo reiniciar. Um deploy, um crash ou um encerramento por falta de memória no meio da chamada perde tudo o que estava em andamento. Num host serverless, o processo também pode ser congelado no instante em que a resposta é enviada.

Python (FastAPI)

from fastapi import BackgroundTasks, FastAPI, Request, Response

app = FastAPI()

@app.post("/webhooks/line")
async def line_webhook(request: Request, background: BackgroundTasks):
    raw = await request.body()
    if not verify_line_signature(raw, request.headers.get("x-line-signature")):
        return Response(status_code=401)
    background.add_task(handle_events, raw)  # runs after the response is sent
    return Response(status_code=200)

O porém é o mesmo do Express: a tarefa vive no processo. Reinicie o processo e a tarefa some, sem nova tentativa e sem nenhum registro.

O que as quatro têm em comum

Cada uma responde 200 a um evento que, naquele momento, existe só na memória. O remetente deixa de se importar, e se o trabalho em segundo plano falhar não há segunda chance nem nada para examinar depois.

A versão durável tem um formato conhecido: grave o evento antes de responder, deixe um worker processá-lo e tentar de novo quando falhar, e guarde registro suficiente para ver o que falhou e enviar de novo. Isso é uma fila com uma lista de dead-letter e um botão de replay (um novo envio de um evento armazenado, feito por você). Não é difícil de construir. É mais uma coisa para operar e monitorar, e mais um motivo para você ser acionado no plantão. E não é para isso que seu bot existe. Nosso guia da Vercel mostra a versão que grava antes de responder, se você quiser manter isso por conta própria.

Se você prefere não operar uma fila

Coloque um relay na frente do handler. No remetente, você troca a URL do webhook pela sua URL do AnyHook, e nada mais.

  • O remetente recebe o 200 em cerca de um segundo. O AnyHook armazena o evento antes de esse 200 sair
  • Depois, seu handler recebe o evento com um prazo de 60 segundos no Free, 120 segundos no Pro e 5 minutos no Scale, dentro da franquia de computação do seu plano. Uma chamada ao modelo cabe nesse prazo, então o handler pode fazer o trabalho na própria requisição e responder quando terminar
  • Se seu handler falhar ou estourar o tempo, o AnyHook tenta de novo no próprio cronograma: 5 min, 30 min, 3 h, 18 h e depois uma vez por dia, até 3 tentativas no Free, 5 no Pro e 10 no Scale
  • Cada evento fica guardado num log pesquisável (3 dias no Free, mais nos planos pagos), e você pode fazer o replay de qualquer um deles depois de corrigir um bug
  • O AnyHook responde ao desafio url_verification do Slack por você, então a URL é salva já na primeira tentativa

O AnyHook faz entrega pelo menos uma vez (at-least-once). Um handler que terminou o trabalho, mas respondeu depois de o prazo acabar, vai receber o evento de novo; então torne-o idempotente: ignore tudo cujo cabeçalho X-AnyHook-Delivery (o id do evento, o mesmo em todas as novas tentativas) você já processou. O id do próprio remetente, como o event_id do Slack ou o X-GitHub-Delivery do GitHub, também é encaminhado. Eventos duplicados e idempotência mostra o padrão.

O que um relay não consegue é responder por você quando o remetente precisa de um corpo de resposta de verdade:

  • Os slash commands e os botões interativos do Slack esperam sua resposta na própria resposta HTTP ou uma confirmação de recebimento rápida. Responda a eles do seu próprio endpoint com um 200 vazio e depois envie a resposta de verdade para o response_url que o Slack manda, que aceita até cinco mensagens em 30 minutos
  • As interações do Discord precisam de uma resposta inicial em até 3 segundos. Responda com uma resposta adiada (tipo 5) e depois edite-a com a resposta do modelo quando ela chegar; o token da interação continua válido por 15 minutos
  • Os webhooks de voz e de mensagens da Twilio esperam TwiML de volta. Responda com TwiML na hora e envie a resposta lenta pela REST API

Um relay serve para webhooks de eventos, o tipo que só precisa de um 2xx: a Events API do Slack, o LINE, a Shopify, o GitHub, a HubSpot, a Stripe e a maioria dos outros.

Meça o tempo do seu handler

Você não precisa adivinhar de que lado da linha seu handler está. O verificador de timeout de webhook envia uma requisição de teste para seu endpoint, mede o tempo e coloca o resultado ao lado do prazo de cada remetente. Ele roda duas vezes, então uma primeira requisição lenta depois de um período parado aparece como o cold start que ela é.

Perguntas frequentes

Por que o Slack diz que meu bot deu timeout, ou por que ele respondeu duas vezes? O Slack espera 3 segundos por um 2xx. Se seu handler chama um modelo antes de responder, ele demora mais, o Slack conta uma falha e envia o evento de novo, e as duas execuções postam uma resposta. Responda primeiro e poste a resposta depois com chat.postMessage, e ignore os eventos que você já viu.

Posso só responder 200 e continuar trabalhando na Vercel? Sim, com a função after() de next/server (ou waitUntil), e o trabalho roda pelo tempo máximo da rota. Não é durável: se o trabalho falhar ou a instância for reciclada, o evento se perde e o remetente não o envia de novo. Serve para uma notificação, não para algo cuja falta um usuário perceberia.

Quanto tempo o LINE espera pela resposta de um webhook? Dois segundos. O LINE conta um webhook que o servidor do seu bot não respondeu em 2 segundos como um request_timeout nas estatísticas de erro e recomenda fazer o trabalho de forma assíncrona. O reenvio de webhooks vem desligado por padrão; ative-o nas configurações da Messaging API do seu canal.

Colocar uma fila na frente deixa meu bot mais lento para responder? Não. O relay encaminha o evento assim que o armazena, então seu handler começa mais ou menos no mesmo momento em que começaria de qualquer forma. O que muda é que o remetente não fica mais segurando a conexão enquanto o modelo pensa. As novas tentativas só acrescentam tempo quando uma entrega falhou, e sem elas aquele evento teria se perdido.

E se meu handler demorar mais de 60 segundos? No Free, o AnyHook espera 60 segundos pela resposta do seu handler; o Pro espera 120 segundos e o Scale, 5 minutos, dentro da franquia de computação do seu plano. Para trabalhos mais longos que isso, responda 202 assim que colocar a tarefa na fila e termine-a do seu lado. O relay conta o 202 como entregue.

Isso funciona para bots do Discord e slash commands do Slack? Não por meio de um relay. Os dois precisam de um corpo de resposta específico em até 3 segundos, que só seu próprio código pode dar. Use a resposta adiada do Discord e o response_url do Slack, como descrito acima. A Events API do Slack, que só precisa de um 2xx, funciona atrás de um relay.

Meu handler pode receber o mesmo evento duas vezes? Pode, com ou sem relay: os remetentes tentam de novo, e o AnyHook também, quando seu handler falha. Trate todo handler como sujeito a entrega pelo menos uma vez e ignore os ids que você já processou.

Todos os posts14 de outubro de 2026 · 9 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.