10 min de lecturaIntegraciones

Webhooks vs polling: seguimiento de clics comparado

Webhooks vs polling para seguir clics: retraso y cuentas de peticiones, reintentos, idempotencia y firmas, con tabla de decisión y código Node que ejecutamos.

Sasha Ehrlich
Compliance · EU residency

Usa webhooks cuando una persona o un flujo de trabajo deba reaccionar en segundos, y haz polling cuando unos minutos de retraso no importen y prefieras no mantener un endpoint público. En Elido la elección tiene una pega: los webhooks de seguimiento de clics todavía no existen. Los webhooks cubren eventos de enlaces, dominios, miembros y espacios de trabajo, y un evento click.created por clic está previsto pero no se emite. Hoy los datos de clics salen del polling de la API de analíticas.

Este artículo hace las cuentas del retraso y del volumen de peticiones, cubre lo que garantiza cada patrón (reintentos, duplicados, orden, firmas) y termina con una tabla de decisión. El código se ejecutó en Node 22 contra mocks locales antes de incluirlo. Para el catálogo completo de eventos de enlaces, lee webhooks para eventos de enlaces; para los endpoints de clics, la guía de la API de analíticas de enlaces.

Webhooks vs polling en un párrafo cada uno

Polling significa que tu código pide datos nuevos según un horario. Una tarea cron llama a GET /v1/workspaces/{workspace_id}/analytics/clicks/recent, procesa lo nuevo y se duerme. Tú inicias cada petición. No hace falta una URL pública.

Los webhooks invierten la dirección. Elido envía un POST firmado con un sobre JSON a tu endpoint HTTPS cuando se dispara un evento, y una respuesta 2xx marca la entrega como hecha. Necesitas un endpoint accesible. TLS. Un manejador que aguante los duplicados. Hoy los eventos son link.created, link.updated, link.expired, domain.verified y el resto de la lista de la documentación de webhooks.

Diagrama lado a lado: en el polling, tu tarea cron envía GET a la API de Elido y extrae filas cada cinco minutos; en los webhooks, el servidor de Elido envía un POST a tu endpoint HTTPS, que devuelve 2xx, con el evento click.created por clic todavía previsto. La página de webhooks de Elido con un endpoint, sus eventos de enlace suscritos, un estado activo y la fecha de creación

La aritmética de latencia y coste

El retraso del polling lo fija el intervalo. Con un intervalo de I minutos, un evento espera I/2 minutos de media y I minutos en el peor caso, más el retraso que tengan los datos antes de poder consultarse. Las peticiones por día son 1.440 / I.

  • 1 minuto: 1.440 peticiones al día, 30 segundos de retraso medio, 1 minuto en el peor caso.
  • 5 minutos: 288 peticiones, 2,5 minutos de media, 5 minutos en el peor caso.
  • 15 minutos: 96 peticiones, 7,5 minutos de media, 15 minutos en el peor caso.
  • 60 minutos: 24 peticiones, 30 minutos de media, 60 minutos en el peor caso.

El coste aparece como peticiones desperdiciadas, no como presión sobre los límites de tasa. El límite publicado de espacio de trabajo en Free es de 10 peticiones por segundo, es decir 864.000 al día (consulta límites de tasa de la API e idempotencia). Un poller con intervalo de 1 minuto usa 1.440 / 864.000 = 0,17 % de eso. Chocarás con los límites cuando hagas polling por enlace en cientos de enlaces, o cuando otra automatización comparta el espacio de trabajo, no con un solo feed.

El desperdicio es la cifra que importa. Supón que un espacio de trabajo cambia 40 enlaces al día y los vigilas con polling cada minuto. Como máximo 40 de las 1.440 peticiones devuelven algo nuevo, así que al menos 1.400 (97 %) están vacías. Un receptor de webhooks recibe 40 peticiones para esos mismos 40 eventos. En el peor caso, con tres intentos cada uno, son 120.

Los webhooks ganan en retraso, que suele ser de segundos porque el worker de entregas recoge continuamente las filas pendientes. Pierden en lo que el polling obtiene gratis: una ejecución perdida se repara con la siguiente, porque los datos siguen en la API. Un webhook perdido solo se repara si construiste algo que lo repare.

Garantías de entrega: qué promete Elido

Estos comportamientos salen del código de entrega y de los documentos de ayuda, comprobados el 1 de octubre de 2026.

Reintentos. Una entrega tiene tres intentos: uno al instante, otro 5 minutos después de un fallo y otro 15 minutos después de ese, unos 20 minutos de principio a fin. Cualquier estado distinto de 2xx, error de red o respuesta de más de 10 segundos es un fallo. Tras el tercero, la entrega se marca como fallida; Retry en el panel o POST .../deliveries/{delivery_id}/retry la rearma con un presupuesto nuevo de tres intentos. A modo de comparación, Stripe reintenta los eventos en modo live hasta tres días. Veinte minutos es poco. No des por hecho que se cubrirá una caída de tu receptor.

Idempotencia. La entrega es de al menos una vez. La cabecera X-Webhook-Delivery identifica una entrega a un endpoint y no cambia entre reintentos automáticos o manuales, así que es tu clave de deduplicación.

Orden. No hay promesa de orden. El reintento de un evento anterior puede llegar después de uno posterior, así que compara data.updated_at con lo que guardaste antes de sobrescribir nada.

Firmas. Cada entrega firmada lleva X-Webhook-Signature (alias X-Elido-Signature) con v1= más el HMAC-SHA256 en hexadecimal de {timestamp}.{raw_body}, y la marca de tiempo en X-Webhook-Timestamp. La marca de tiempo es nueva en cada intento, así que un reintento legítimo nunca está caducado. Durante una rotación de secreto, X-Elido-Signature-Previous lleva durante siete días una firma del secreto antiguo. Verificar firmas de webhooks cubre Python, Go y la depuración de una discrepancia.

Transporte y pausa. Los endpoints de eventos nuevos deben usar https; http plano solo se acepta para localhost y 127.0.0.1. Poner is_active en false pausa un endpoint. Desde la corrección del 22 de septiembre, eso detiene también las entregas en cola.

Un receptor de webhooks en Node

Este receptor verifica la firma sobre los bytes en bruto, impone una ventana de cinco minutos, acepta la cabecera del secreto anterior durante la rotación y deduplica por el ID de entrega. La tolerancia de cinco minutos es decisión tuya, no de Elido, y coincide con el valor por defecto de Stripe. Lo ejecuté contra un firmador local: una petición válida devolvió 200, un reenvío con el mismo ID de entrega devolvió "duplicate", y un secreto erróneo y una marca de tiempo de 400 segundos de antigüedad devolvieron 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);

La comprobación de longitud antes de timingSafeEqual importa. Node lanza una excepción con buffers de distinto tamaño. Responde primero, trabaja después. El corte de 10 segundos es donde un manejador lento pero correcto se convierte en un duplicado.

Un poller de clics en Node

Hasta que se publique click.created, esta es la vía de los clics. clicks/recent devuelve primero lo más reciente, acepta limit hasta 500 (100 por defecto) y devuelve next_cursor siempre que una página llega llena. Cada ejecución lee desde arriba y se detiene al llegar al clic más reciente que ya procesó. Lo ejecuté contra un mock que pagina de dos en dos filas: la primera ejecución procesó 5 clics, la segunda solo los 2 nuevos y la tercera ninguno.

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);

Dos límites, dichos con claridad. El cursor ordena por marca de tiempo e ID de enlace, así que dos clics en un mismo enlace en el mismo segundo comparten clave; si no puedes permitirte un clic omitido o repetido en raras ocasiones, haz handle idempotente sobre la fila completa. Y lastSeen vive en memoria, así que persístelo entre reinicios, o un reinicio repetirá el historial reciente.

¿No estás listo para construir ninguno? No pasa nada. Abre el panel y usa primero las analíticas del panel, y añade la API cuando sepas qué números necesitas de forma programada.

Tabla de decisión

SituaciónQué usar hoyPor qué
Reaccionar en segundos a cambios de enlaces o dominiosWebhook en link.*, domain.*Los eventos se envían; no hace falta un evento de clic
Contador de clics en vivo en tu productoPolling de 1 minuto de clicks/recentTodavía no hay webhook de clic; 1.440 peticiones al día queda muy dentro del límite
Enriquecimiento de CRM por clicReenviador de eventos o polling cortoUn retraso de polling de 30 segundos de media suele ser aceptable
Informes semanales o mensualesPolling de summary y timeseriesLos datos retrospectivos no ganan nada con el push
No hay endpoint público disponiblePollingLos webhooks necesitan una URL https accesible desde internet
No puedes perder un evento de enlaceWebhook más conciliación nocturnaLos reintentos cubren unos 20 minutos, el polling cubre el resto

Cuando se publique el flujo muestreado de click.created, las filas del contador en vivo y del CRM pasarán a la columna de webhook, y el polling se convertirá en la pasada de conciliación. Hasta entonces, los reenviadores de eventos como eventos de clic de enlaces en Mixpanel son la opción tipo push para los datos de clics.

El patrón híbrido

Para todo lo que no puedas permitirte perder, ejecuta ambos. El webhook es la vía rápida. Un polling programado compara totales y encuentra huecos: entregas que agotaron sus tres intentos durante una caída, o eventos que llegaron desordenados.

Los informes summary y timeseries devuelven totales y agrupaciones diarias de un rango de fechas en una sola llamada, lo que abarata una comparación nocturna. Recuerda que el filtro de bots elimina rastreadores y monitores antes de registrar los clics, así que tus totales no coincidirán con los registros brutos del servidor; la guía de analíticas explica qué se excluye. La lista de comprobación de seguridad cubre el resto. Si diseñas tus propios webhooks, la especificación Standard Webhooks es una referencia útil.

Página de documentación de la API en el panel de Elido con enlaces a la referencia de la API, los SDK, la especificación OpenAPI y la gestión de claves de API

La pantalla de documentación de la API de un espacio de trabajo DEMO con datos de ejemplo; no es una cuenta de cliente.

Lista de comprobación previa al lanzamiento para cualquiera de los patrones

Antes de lanzar un receptor o un poller, repasa seis comprobaciones. La mayoría de los incidentes de producción con ambos patrones vienen de saltarse una de ellas.

  • Webhook: responde con 2xx en menos de 10 segundos y haz el trabajo lento después de responder.
  • Webhook: verifica la firma sobre el cuerpo en bruto antes de analizar el JSON.
  • Webhook: guarda X-Webhook-Delivery con un índice único para que un reintento no pueda contar doble.
  • Poller: persiste el cursor o la última clave vista fuera de la memoria del proceso.
  • Poller: aplica retroceso ante un 429 o 5xx en lugar de reintentar con el intervalo normal.
  • Ambos: alerta cuando no haya llegado ningún evento o clic nuevo durante más que tu periodo normal de calma, porque el silencio es el aspecto que tiene una integración rota. Textos independientes como la guía de webhook vs API polling de Hookdeck llegan al mismo consejo: push para la velocidad, polling para conciliar.

Relacionado en el blog

Preguntas frecuentes

¿Cuál es la diferencia entre webhooks y polling?

Con polling, tu código pide datos nuevos a la API según un horario. Con webhooks, el servidor envía un HTTP POST a tu endpoint cuando algo ocurre. El polling cuesta peticiones y añade hasta un intervalo de retraso. Los webhooks cuestan un endpoint público siempre activo y el trabajo de gestionar reintentos y duplicados.

¿Tiene Elido webhooks para el seguimiento de clics?

Todavía no. Los webhooks cubren eventos de enlaces, dominios, miembros y espacios de trabajo, como link.created y link.expired. Está previsto un evento click.created, pero hoy nada lo publica. Para datos a nivel de clic, consulta por polling el informe clicks/recent de la API de analíticas con su cursor, o usa un reenviador de eventos.

¿Cada cuánto debo consultar por polling una API de analíticas?

Ajusta el intervalo a la decisión que alimentan los datos. Un minuto sirve para un contador casi en vivo, de cinco a quince minutos para paneles, y cada hora o cada día para informes. Un polling de 1 minuto hace 1.440 peticiones al día y uno de 5 minutos hace 288, así que rara vez compensa acortar más.

¿Se garantiza que los webhooks lleguen exactamente una vez?

No. La entrega de webhooks es de al menos una vez, así que un receptor ve duplicados tras un timeout o un reenvío manual. Elido mantiene el mismo valor de X-Webhook-Delivery entre reintentos, así que guarda ese ID y omite todo lo que ya hayas procesado.

¿Qué pasa si mi endpoint de webhooks está caído?

Elido hace tres intentos: uno inmediato, otro cinco minutos después de un fallo y otro quince minutos después de ese. Si los tres fallan, la entrega se marca como fallida hasta que alguien pulse Retry o llame al endpoint de reintento. Las caídas de más de unos veinte minutos necesitan un trabajo de conciliación.

¿Puedo combinar webhooks y polling?

Sí, y para todo lo que importe deberías hacerlo. Usa el webhook como vía rápida y un polling programado como red de seguridad que encuentre los eventos que tu receptor perdió. El polling puede ejecutarse cada hora o cada noche porque solo tiene que cerrar huecos.

Prueba Elido

Pega una URL, obtén un enlace corto

Sin registro. El enlace vive 30 días. Crea una cuenta para conservarlo.

Sin registro. Crea una cuenta gratis y obtén 50 enlaces al mes.

Prueba Elido

Acortador de URL alojado en la UE: dominios personalizados, análisis profundo y API abierta. Plan gratuito - sin tarjeta de crédito.

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

Seguir leyendo