10 min de leituraIntegrações

Webhooks vs polling: rastreamento de cliques comparado

Webhooks vs polling no rastreamento de cliques: latência e conta de requisições, retries, idempotência, assinaturas, tabela de decisão e código Node testado.

Sasha Ehrlich
Compliance · EU residency

Use webhooks quando uma pessoa ou um fluxo de trabalho precisa reagir em segundos, e faça polling quando alguns minutos de atraso são aceitáveis e você prefere não manter um endpoint público. No Elido a escolha tem uma pegadinha: webhooks de rastreamento de cliques ainda não existem. Os webhooks cobrem eventos de link, domínio, membro e workspace, e um evento click.created por clique está planejado, mas não é emitido. Hoje os dados de cliques vêm do polling da API de analytics.

Este post faz a aritmética de atraso e volume de requisições, cobre o que cada padrão garante (retries, duplicatas, ordem, assinaturas) e termina com uma tabela de decisão. O código foi executado no Node 22 contra mocks locais antes de entrar aqui. Para o catálogo completo de eventos de link, leia webhooks para eventos de link; para os endpoints de cliques, o guia da API de analytics de links.

Webhooks vs polling em um parágrafo cada

Polling significa que o seu código pede novos dados em intervalos regulares. Um job cron chama GET /v1/workspaces/{workspace_id}/analytics/clicks/recent, trata o que é novo e dorme. Você inicia toda requisição. Não precisa de URL pública.

Os webhooks invertem o sentido. O Elido envia um POST assinado com um envelope JSON ao seu endpoint HTTPS quando um evento dispara, e uma resposta 2xx marca a entrega como concluída. Você precisa de um endpoint acessível. TLS. Um handler que sobreviva a duplicatas. Hoje os eventos são link.created, link.updated, link.expired, domain.verified e o resto da lista na documentação de webhooks.

Diagrama lado a lado: no polling, o seu job cron envia GET à API do Elido e puxa linhas a cada cinco minutos; nos webhooks, o servidor do Elido envia um POST ao seu endpoint HTTPS, que devolve 2xx, com o evento click.created por clique ainda planejado. A página de webhooks do Elido listando um endpoint com seus eventos de link assinados, um status ativo e a data de criação

A aritmética de latência e custo

O atraso do polling é fixado pelo intervalo. Com um intervalo de I minutos, um evento espera I/2 minutos em média e I minutos no pior caso, mais o atraso que os dados já têm antes de poderem ser consultados. As requisições por dia são 1.440 / I.

  • 1 minuto: 1.440 requisições por dia, 30 segundos de atraso médio, 1 minuto no pior caso.
  • 5 minutos: 288 requisições, 2,5 minutos em média, 5 minutos no pior caso.
  • 15 minutos: 96 requisições, 7,5 minutos em média, 15 minutos no pior caso.
  • 60 minutos: 24 requisições, 30 minutos em média, 60 minutos no pior caso.

O custo aparece como requisições desperdiçadas, não como pressão sobre o rate limit. O limite publicado do workspace no plano Free é de 10 requisições por segundo, o que dá 864.000 por dia (veja rate limits e idempotência da API). Um poller com intervalo de 1 minuto usa 1.440 / 864.000 = 0,17% disso. Você vai bater nos limites ao fazer polling por link em centenas de links, ou quando outras automações compartilham o workspace, não com um único feed.

O desperdício é o número que importa. Digamos que um workspace altere 40 links por dia e você acompanhe isso fazendo polling a cada minuto. No máximo 40 das 1.440 requisições trazem algo novo, então pelo menos 1.400 (97%) voltam vazias. Um receptor de webhook recebe 40 requisições para os mesmos 40 eventos. No pior caso, com três tentativas cada, são 120.

Os webhooks ganham na latência, que costuma ser de segundos porque o worker de entrega pega as linhas pendentes continuamente. Perdem naquilo que o polling recebe de graça: uma execução perdida é reparada pela execução seguinte, porque os dados ainda estão na API. Um webhook perdido só é reparado se você construiu algo para repará-lo.

Garantias de entrega: o que o Elido promete

Estes comportamentos vêm do código de entrega e dos documentos de ajuda, conferidos em 1 de outubro de 2026.

Retries. Uma entrega recebe três tentativas: uma imediata, uma 5 minutos após uma falha e uma 15 minutos depois dessa, cerca de 20 minutos de ponta a ponta. Qualquer status que não seja 2xx, erro de rede ou resposta mais lenta que 10 segundos é uma falha. Depois da terceira, a entrega é marcada como falha; Retry no painel ou POST .../deliveries/{delivery_id}/retry a rearma com um novo orçamento de três tentativas. Para comparação, o Stripe faz retries de eventos em modo live por até três dias. Vinte minutos é pouco. Não presuma que uma queda do receptor será coberta.

Idempotência. A entrega é at least once. O cabeçalho X-Webhook-Delivery identifica uma entrega para um endpoint e não muda entre retries automáticos ou manuais, então é a sua chave de deduplicação.

Ordem. Não há promessa de ordem. O retry de um evento antigo pode chegar depois de um mais novo, então compare data.updated_at com o que você armazenou antes de sobrescrever qualquer coisa.

Assinaturas. Toda entrega assinada leva X-Webhook-Signature (alias X-Elido-Signature) com v1= mais o HMAC-SHA256 em hexadecimal de {timestamp}.{raw_body}, e o timestamp em X-Webhook-Timestamp. O timestamp é novo a cada tentativa, então um retry legítimo nunca fica obsoleto. Durante uma rotação de segredo, X-Elido-Signature-Previous carrega por sete dias uma assinatura feita com o segredo antigo. Verificar assinaturas de webhook cobre Python, Go e como depurar uma divergência.

Transporte e pausa. Novos endpoints de eventos precisam usar https; http simples só é aceito para localhost e 127.0.0.1. Definir is_active como false pausa um endpoint. Desde a correção de 22 de setembro, isso também interrompe as entregas na fila.

Um receptor de webhook em Node

Este receptor verifica a assinatura sobre os bytes brutos, impõe uma janela de cinco minutos, aceita o cabeçalho do segredo anterior durante a rotação e deduplica pelo ID da entrega. A tolerância de cinco minutos é escolha sua, não do Elido, e corresponde ao padrão do Stripe. Executei-o contra um assinador local: uma requisição válida devolveu 200, um replay com o mesmo ID de entrega devolveu "duplicate", e um segredo errado e um timestamp de 400 segundos atrás devolveram 401.

import http from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.WEBHOOK_SECRET; // the whsec_... value
const seen = new Set(); // use a unique index in a real database

export function verify(secret, headers, rawBody, toleranceSec = 300) {
  const ts = headers["x-webhook-timestamp"] ?? "";
  if (!/^\d+$/.test(ts)) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) return false;
  const mac = createHmac("sha256", secret).update(`${ts}.`).update(rawBody);
  const expected = Buffer.from("v1=" + mac.digest("hex"));
  return ["x-elido-signature", "x-elido-signature-previous"].some((name) => {
    const got = Buffer.from(headers[name] ?? "");
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}

http
  .createServer((req, res) => {
    const chunks = [];
    req.on("data", (c) => chunks.push(c));
    req.on("end", () => {
      const raw = Buffer.concat(chunks);
      if (req.method !== "POST" || !verify(SECRET, req.headers, raw)) {
        return res.writeHead(401).end();
      }
      const id = req.headers["x-webhook-delivery"];
      if (seen.has(id)) return res.writeHead(200).end("duplicate");
      seen.add(id);
      const event = JSON.parse(raw.toString("utf8"));
      queueMicrotask(() => console.log("handled", event.type)); // do the work after replying
      res.writeHead(200).end("ok");
    });
  })
  .listen(8787);

A verificação de tamanho antes de timingSafeEqual importa. O Node lança uma exceção com buffers de tamanhos diferentes. Responda primeiro, trabalhe depois. O corte de 10 segundos é onde um handler lento, mas bem-sucedido, vira uma duplicata.

Um poller de cliques em Node

Até o click.created ser lançado, este é o caminho dos cliques. clicks/recent devolve do mais novo para o mais antigo, aceita limit até 500 (padrão 100) e devolve next_cursor sempre que uma página volta cheia. Cada execução lê a partir do topo e para ao chegar ao clique mais novo que já tratou. Executei-o contra um mock que pagina duas linhas por vez: a primeira execução tratou 5 cliques, a segunda tratou apenas os 2 novos, a terceira não tratou nenhum.

const BASE = "https://api.elido.app";
const WS = process.env.ELIDO_WORKSPACE_ID;
const KEY = process.env.ELIDO_API_KEY;

// Newest click already handled; clicks/recent is newest first.
let lastSeen = null;
const keyOf = (c) => `${c.ts}|${c.link_id}`;

export async function pollOnce(handle) {
  let cursor = null;
  let newest = null;
  for (;;) {
    const url = new URL(`${BASE}/v1/workspaces/${WS}/analytics/clicks/recent`);
    url.searchParams.set("limit", "500");
    if (cursor) url.searchParams.set("cursor", cursor);
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${KEY}` },
      signal: AbortSignal.timeout(10_000),
    });
    if (!res.ok) throw new Error(`clicks/recent returned ${res.status}`);
    const { items, next_cursor } = await res.json();
    for (const click of items) {
      if (keyOf(click) === lastSeen) {
        if (newest) lastSeen = newest;
        return;
      }
      newest ??= keyOf(click);
      await handle(click);
    }
    if (!next_cursor) break;
    cursor = next_cursor;
  }
  if (newest) lastSeen = newest;
}

// setInterval(() => pollOnce(handleClick).catch(console.error), 60_000);

Dois limites, ditos com clareza. O cursor ordena por timestamp e ID do link, então dois cliques no mesmo link no mesmo segundo compartilham uma chave; se você não pode se dar ao luxo de um clique raramente pulado ou repetido, torne handle idempotente sobre a linha completa. E lastSeen vive na memória, então persista-o entre reinicializações, ou uma reinicialização reprocessa o histórico recente.

Ainda não está pronto para construir nenhum dos dois? Tudo bem. Abra o painel e use primeiro os analytics do painel, depois adicione a API quando souber quais números você precisa de forma agendada.

Tabela de decisão

SituaçãoUse hojePor quê
Reagir a mudanças de link ou domínio em segundosWebhook em link.*, domain.*Os eventos são enviados; nenhum evento de clique é necessário
Contador de cliques ao vivo no seu produtoPolling de 1 minuto em clicks/recentAinda não há webhook de clique; 1.440 requisições por dia cabem bem no limite
Enriquecimento de CRM por cliqueEncaminhador de eventos ou polling curtoUm atraso médio de 30 segundos no polling costuma ser aceitável
Relatórios semanais ou mensaisPolling de summary e timeseriesDados retrospectivos não ganham nada com push
Nenhum endpoint público disponívelPollingWebhooks precisam de uma URL https acessível pela internet
Não pode perder um evento de linkWebhook mais reconciliação noturnaOs retries cobrem cerca de 20 minutos, o polling cobre o resto

Quando o fluxo amostrado de click.created for lançado, as linhas do contador ao vivo e do CRM passam para a coluna de webhook, e o polling vira a passada de reconciliação. Até lá, encaminhadores de eventos como eventos de clique de link no Mixpanel são a opção estilo push para dados de cliques.

O padrão híbrido

Para tudo o que você não pode se dar ao luxo de perder, rode os dois. O webhook é o caminho rápido. Um polling agendado compara totais e encontra lacunas: entregas que esgotaram suas três tentativas durante uma queda, ou eventos que chegaram fora de ordem.

Os relatórios summary e timeseries devolvem totais e intervalos diários para um período em uma chamada, o que torna barata uma comparação noturna. Lembre-se de que o filtro de bots remove rastreadores e monitores antes de os cliques serem registrados, então os seus totais não vão bater com os logs brutos do servidor; o guia de analytics explica o que é excluído. A checklist de segurança cobre o resto. Se você desenhar seus próprios webhooks, a especificação Standard Webhooks é uma referência útil.

Página de documentação da API no painel do Elido com links para a referência da API, os SDKs, a especificação OpenAPI e o gerenciamento de chaves de API

A tela de documentação da API de um workspace DEMO com dados de exemplo; não é uma conta de cliente.

Uma checklist pré-lançamento para qualquer um dos padrões

Antes de colocar um receptor ou um poller no ar, passe por seis verificações. A maioria dos incidentes em produção com os dois padrões vem de pular uma delas.

  • Webhook: responda com 2xx em até 10 segundos e faça o trabalho lento depois de responder.
  • Webhook: verifique a assinatura sobre o corpo bruto antes de fazer o parse do JSON.
  • Webhook: guarde X-Webhook-Delivery com um índice único para que um retry não conte em dobro.
  • Poller: persista o cursor ou a chave do último item visto fora da memória do processo.
  • Poller: faça backoff diante de um 429 ou 5xx em vez de tentar de novo no intervalo normal.
  • Ambos: dispare um alerta quando nenhum evento ou clique novo chegar por mais tempo do que o seu período normal de silêncio, porque o silêncio é a cara de uma integração quebrada. Textos independentes como o guia de webhook vs API polling da Hookdeck chegam ao mesmo conselho: push para velocidade, polling para reconciliar.

Relacionados no blog

Perguntas frequentes

Qual é a diferença entre webhooks e polling?

Com polling, o seu código pede novos dados à API em intervalos regulares. Com webhooks, o servidor envia um HTTP POST ao seu endpoint quando algo acontece. O polling custa requisições e adiciona até um intervalo de atraso. Os webhooks custam um endpoint público sempre no ar e o trabalho de lidar com retries e duplicatas.

O Elido tem webhooks de rastreamento de cliques?

Ainda não. Os webhooks cobrem eventos de link, domínio, membro e workspace, como link.created e link.expired. Um evento click.created está planejado, mas nada o publica hoje. Para dados no nível do clique, faça polling do relatório clicks/recent da API de analytics com o cursor dele, ou use um encaminhador de eventos.

Com que frequência devo fazer polling de uma API de analytics?

Ajuste o intervalo à decisão que os dados alimentam. Um minuto serve para um contador quase ao vivo, de cinco a quinze minutos servem para dashboards, e de hora em hora ou diário servem para relatórios. Um polling de 1 minuto faz 1.440 requisições por dia e um de 5 minutos faz 288, então mais curto raramente compensa.

Os webhooks têm garantia de chegar exatamente uma vez?

Não. A entrega de webhooks é at least once (pelo menos uma vez), então um receptor vê duplicatas depois de um timeout ou de um reenvio manual. O Elido mantém o mesmo valor de X-Webhook-Delivery entre os retries, então guarde esse ID e ignore o que você já tratou.

O que acontece se o meu endpoint de webhook estiver fora do ar?

O Elido faz três tentativas: uma imediata, outra cinco minutos após uma falha e outra quinze minutos depois dessa. Se as três falharem, a entrega fica marcada como falha até alguém clicar em Retry ou chamar o endpoint de retry. Quedas de mais de cerca de vinte minutos exigem um job de reconciliação.

Posso combinar webhooks e polling?

Sim, e para tudo o que importa você deveria. Use o webhook como o caminho rápido e um polling agendado como a rede de segurança que encontra os eventos que o seu receptor perdeu. O polling pode rodar de hora em hora ou à noite, porque só precisa fechar lacunas.

Experimente Elido

Cole uma URL, obtenha um link curto

Sem cadastro. O link vive 30 dias. Cadastre-se para mantê-lo para sempre.

Sem registro. Crie uma conta grátis e obtenha 50 links por mês.

Experimente o Elido

Encurtador de URL hospedado na UE: domínios personalizados, análises profundas e API aberta. Plano gratuito - sem cartão de crédito.

Tags
webhooks vs polling
click tracking webhooks
click tracking api
url shortener webhooks
webhook retries idempotency
link analytics api

Continuar lendo