Seu bot funciona perfeitamente em desenvolvimento. Em produção, ele responde às vezes. Os usuários contam que ele "geralmente" responde, ou que respondeu três vezes a uma mensagem, ou que ficou em silêncio uma tarde inteira e voltou sozinho.
Os três são o mesmo bug. O Slack deu três segundos, seu handler levou quatro, e tudo o que vem depois é a política de novas tentativas (retries) do Slack interagindo com um handler que não foi escrito para esperar por ela.
TL;DR
- "Seu app deve responder à requisição do evento com um HTTP 2xx em até três segundos"
- O Slack tenta de novo três vezes: quase imediatamente, depois de 1 minuto e depois de 5 minutos
- Um handler não idempotente transforma essas novas tentativas em mensagens duplicadas que as pessoas veem
x-slack-no-retry: 1numa resposta diferente de 200 interrompe as novas tentativas de eventos que você sabe que não consegue tratar- Se mais de 95% das entregas falharem numa janela de 60 minutos, o Slack desativa suas inscrições de eventos e manda um e-mail para o dono do app
Três segundos não dão para quase nada
Veja o que um handler plausível faz entre receber message.channels e estar pronto para responder. Ele verifica a assinatura. Descobre de qual workspace é o evento. Lê algum estado do seu banco de dados. Depois, se o bot faz qualquer coisa interessante em 2026, ele chama um modelo.
Os três primeiros passos levam uns 200 ms num dia bom. O quarto leva 30 segundos.
É o mesmo descompasso de escala de tempo que aparece com webhooks de pagamento, só que aqui o prazo de resposta são três segundos documentados (o prazo de cada remetente está na nossa referência de provedores de webhook), e a consequência é visível, não silenciosa. Ninguém vê um payment_intent.succeeded falhar. Todo mundo vê um bot não responder.
O formato geral da correção, para qualquer remetente e qualquer plataforma em que você possa hospedar o handler, está em Seu handler de webhook chama um LLM, e o verificador de timeout mede seu handler contra os três segundos do Slack. O Slack também tem uma saída de emergência específica que torna tudo bem menos doloroso do que com a maioria dos remetentes.
O truque que o Slack oferece e a Stripe não
O Slack separa confirmar o recebimento do evento de responder ao usuário. Você responde 200 imediatamente e publica a resposta de verdade depois, usando chat.postMessage ou chamando o response_url que os payloads interativos trazem.
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);
// URL verification handshake, only during setup
if (body.type === "url_verification") {
return Response.json({ challenge: body.challenge });
}
await queue.publish(body); // durable, ~10ms
return new Response(null, { status: 200 }); // well inside 3s
}
O worker então leva o tempo que precisar e chama chat.postMessage quando tiver algo a dizer. Do lado do usuário, isso parece o bot pensando, o que é normal para um bot. Do lado do Slack, o recebimento do evento foi confirmado em 40 milissegundos.
Se o trabalho vai levar mais de um ou dois segundos, publique na hora uma mensagem provisória e atualize-a depois. Uma mensagem que diz "trabalhando nisso" e depois se edita é uma experiência muito melhor do que três segundos de nada seguidos de um timeout.
O que as novas tentativas fazem com você de verdade
O cronograma do Slack é curto e específico: "A primeira nova tentativa será enviada quase imediatamente. A segunda nova tentativa será feita depois de 1 minuto. A terceira e última nova tentativa será enviada depois de 5 minutos."
Agora pense num handler que faz o trabalho primeiro, responde 200 por último e leva quatro segundos. O Slack dá timeout em três. Seu handler termina em quatro e publica uma mensagem. O Slack tenta de novo quase imediatamente, seu handler roda de novo, publica de novo. Um minuto depois, uma terceira mensagem. Cinco minutos depois, uma quarta.
O usuário fez uma pergunta e recebeu quatro respostas, espalhadas ao longo de seis minutos, o que parece menos um bot lento e mais um bot quebrado.
A correção é o mesmo padrão de restrição única de sempre, com o event_id do Slack como chave:
const { rowCount } = await db.query(
`INSERT INTO processed_events (event_id, provider)
VALUES ($1, 'slack') ON CONFLICT (event_id) DO NOTHING`,
[body.event_id],
);
if (rowCount === 0) return new Response(null, { status: 200 });
Repare no 200 no caminho da duplicata. Responder com erro ali diz ao Slack para tentar de novo aquilo que você acabou de ignorar com sucesso. Mais sobre o caso de concorrência, em que duas novas tentativas chegam a duas instâncias no mesmo milissegundo, em Você recebeu o mesmo webhook duas vezes.
O cabeçalho que ninguém usa
Alguns eventos nunca vão dar certo. O workspace desinstalou seu app, o canal foi arquivado, o payload faz referência a um objeto que você excluiu. Tentar de novo esses eventos desperdiça três entregas e, mais importante, empurra sua taxa de falhas para o limite que faz suas inscrições serem desligadas.
O Slack permite recusar as novas tentativas resposta por resposta: "Envie este cabeçalho HTTP e este valor como parte da sua resposta diferente de 200 OK: x-slack-no-retry: 1".
if (!workspace) {
return new Response("unknown workspace", {
status: 410,
headers: { "x-slack-no-retry": "1" },
});
}
Use-o só para condições definitivas. Um timeout do banco de dados não é definitivo, e você quer essa nova tentativa.
O limite de falhas
Esta é a parte que transforma uma tarde lenta numa queda.
"Se seu app entrar em qualquer combinação dessas condições de falha em mais de 95% das tentativas de entrega dentro de 60 minutos, as inscrições de eventos do seu app serão desativadas temporariamente."
Quando isso acontece, o Slack manda um e-mail para quem criou o app e para o dono dele. O que só é útil se esse endereço pertencer a alguém que ainda está na empresa e ainda o lê. É a mesma armadilha do e-mail de emergência para desenvolvedores da Shopify, descrita em A Shopify excluiu sua inscrição de webhook: o único aviso que você recebe vai para um endereço escolhido durante a configuração e nunca mais revisto.
Vale internalizar: 95% em uma hora não é uma degradação lenta que você vai perceber. Um deploy que quebra a verificação de assinatura chega a 100% na hora, e uma hora depois as inscrições estão desligadas. O bot não dá erro. Ele simplesmente para de receber qualquer coisa, o que, visto de dentro, parece exatamente um canal tranquilo.
Como depurar um bot que responde às vezes
Confira nesta ordem.
O tempo de resposta, não a taxa de erros. Um handler em 2,8 segundos passa em todos os testes que você escreve e falha sempre que o banco de dados fica um pouco ocupado. Meça o p99, não a média, porque é no p99 que o Slack dá timeout.
Cold starts, se você está em serverless. Dois segundos de cold start num prazo de três segundos deixam um segundo para todo o resto. Como receber webhooks na Vercel sem perder nenhum mostra como manter pequeno o grafo de imports da rota, o que devolve mais prazo do que qualquer outra coisa que você possa fazer aqui.
Se as duplicatas são novas tentativas do Slack. Uma mensagem duplicada seis minutos depois da primeira é a terceira nova tentativa do Slack, não um bug na sua lógica de publicação. O intervalo entre elas diz qual é o caso.
Se a inscrição continua ativa. Se tudo ficou em silêncio de uma vez e nada mudou nos seus logs, confira a página Event Subscriptions (inscrições de eventos) do app antes de depurar qualquer outra coisa.
Onde o AnyHook entra
Três segundos é uma restrição de transporte, e transporte é o que um relay de webhooks (um serviço que recebe, guarda e encaminha seus webhooks) substitui.
Aponte a Request URL do Slack para in.anyhook.net/you/slack. O AnyHook verifica e armazena o evento na edge (o Cloudflare Worker do AnyHook) e responde 200 em cerca de um segundo, esteja seu handler quente, frio ou pegando fogo. Quem cumpre o relógio de três segundos do Slack é um worker na edge que só faz duas coisas, então a sequência de novas tentativas quase nunca começa e você nunca chega perto do limite de 95%.
- Seu handler ganha de 60 a 300 segundos em vez de três, conforme o plano e dentro da franquia de computação dele
- O AnyHook armazena cada evento antes da entrega, então uma janela de deploy vira um replay (um novo envio de um evento armazenado, feito por você), e não uma série de perguntas dos seus usuários
- A entrega para seu endpoint é tentada de novo com backoff exponencial por muito mais tempo do que a sequência de seis minutos do Slack
- Alertas de falha disparam com 1, 5 e 20 falhas consecutivas, bem antes de qualquer coisa chegar a um limite que faça o Slack agir
O AnyHook responde ao handshake de url_verification na edge, depois de verificar a assinatura do Slack se você tiver informado seu segredo de assinatura, e não encaminha nada ao seu handler.
Conclusão
Os três segundos do Slack são o prazo mais apertado que qualquer grande remetente impõe, e um dos poucos em que perder o prazo fica visível para quem usa seu produto. Confirme o recebimento primeiro e responda depois, sempre. Deduplique por event_id antes de fazer qualquer coisa que um humano veria duas vezes. Envie x-slack-no-retry: 1 quando souber que o evento está morto. E vá conferir qual endereço está no app como dono, porque é lá que chega o aviso quando suas inscrições são desligadas.