Elido
12 min de lecturaIntegraciones

Webhooks vs polling para tracking de clics - elige el patrón correcto

Un desglose práctico de cuándo usar webhooks y cuándo hacer polling a la API de analítica para datos de clics: costes ocultos de cada enfoque, ejemplos concretos de código en TypeScript y Python, y el patrón híbrido que cubre la mayoría de los casos de uso en producción.

Sasha Ehrlich
Compliance · EU residency
Split diagram comparing webhooks and polling for click tracking: left panel shows server pushing click.recorded events to receiver endpoint; right panel shows cron hitting /analytics/summary every N minutes

Dos equipos construyendo sobre la misma API de acortador de URL a menudo terminan con arquitecturas de integración completamente diferentes. Un equipo configura un endpoint de webhook y reacciona a cada clic en tiempo real. El otro escribe un cron job que hace polling a la API de analítica cada cinco minutos. Ambos son válidos. La decisión entre ellos tiene consecuencias reales para latencia, sobrecarga operacional, y cuánto se degrada tu sistema cuando algo sale mal.

Este post expone los trade-offs reales.

Los dos patrones#

Polling#

Polling significa que tu código pregunta a la API por datos recientes de clics en un horario. Un cron job se despierta, llama a /v1/analytics/workspaces/{id}/clicks/recent o /v1/analytics/workspaces/{id}/summary, procesa los resultados, y luego duerme hasta el siguiente intervalo.

El flujo de datos es basado en pull: tu infraestructura inicia cada interacción. El servidor de la API no tiene conocimiento de tus sistemas internos - solo responde a las consultas que envías.

Webhooks#

Webhooks significa que el servidor de Elido envía un evento click.recorded a tu endpoint HTTPS poco después de que un clic es procesado. Tu receptor lo maneja, devuelve un 2xx, y la entrega se registra como exitosa.

El flujo de datos es basado en push: la plataforma inicia el contacto. Tu endpoint necesita ser alcanzable desde internet, necesita TLS, y necesita responder de forma fiable.

Diagrama lado a lado: el polling muestra tu cron job enviando GET a la API de Elido y extrayendo filas cada cinco minutos; los webhooks muestran el servidor de Elido enviando un POST click.recorded a tu endpoint HTTPS que devuelve 2xx.

Cuándo el polling es la elección correcta#

El polling se adapta a un conjunto específico de condiciones. Si la mayoría de estas aplican a tu situación, empieza con polling y recurre a webhooks solo cuando un problema concreto fuerce tu mano.

Controlas ambos lados de la integración. Cuando el consumidor es un dashboard o herramienta de reporting que tú posees y operas, el polling te da un comportamiento predecible y acotado. Tú decides el intervalo; tú decides la ventana de tiempo; tú decides cómo manejar resultados parciales.

Tu caso de uso es retrospectivo. Los informes semanales de campaña, los trabajos de agregación mensuales, y las pipelines de reconciliación no se benefician de latencia sub-minuto. Un cron job corriendo cada hora contra /summary o /breakdown/country es arquitectónicamente más simple y más fácil de razonar que un receptor de webhook con estado con manejo de reintentos.

No tienes un endpoint público que exponer. Los webhooks requieren una URL alcanzable desde la infraestructura de Elido. Si tu integración corre dentro de una red privada, una función Lambda sin URL estable, o la máquina local de un desarrollador, configurar un endpoint HTTPS entrante puede costar más en complejidad operacional que lo que vale el beneficio de latencia.

El volumen es bajo. A unos pocos miles de clics por día, la diferencia entre tiempo real y un retraso de cinco minutos rara vez es visible para los usuarios finales. El polling es simple de entender, simple de debuggear, y no produce sorpresas de infraestructura.

Cuándo los webhooks son la elección correcta#

Los webhooks tienen sentido cuando la latencia es un requisito de producto en lugar de un nice-to-have.

Estás construyendo un contador en vivo o UX en tiempo real. Si tu producto muestra a los usuarios un conteo de clics que se actualiza visiblemente en segundos después de una redirección, el polling en cualquier intervalo razonable se sentirá notablemente obsoleto. Un handler de webhook que incrementa un contador de Redis en eventos click.recorded y lo muestra a través de una conexión WebSocket o SSE al frontend es la arquitectura que logra esto sin martillar la API de analítica.

Estás enriqueciendo registros de CRM por clic. Atar un evento de clic a un registro de contacto - identificar qué prospecto específico siguió el enlace en tu email de outbound y actualizar su timeline de CRM - es sensible al tiempo. Para cuando un trabajo de polling se ponga al día cinco minutos después, el representante de ventas puede haber ya llamado. Un handler de webhook que dispara una actualización de CRM en segundos del clic es la herramienta correcta.

Estás ejecutando workflows event-driven. Los workflows disparados por eventos de clic - enviar un email de seguimiento cuando se clica un enlace, actualizar el segmento de un suscriptor, decrementar un conteo de inventario - son consumidores naturales de webhook. El evento click.recorded lleva suficientes datos para actuar inmediatamente, sin una consulta de ida y vuelta.

Tienes un endpoint HTTPS estable y públicamente alcanzable. Este es el prerrequisito del que todo lo demás depende. Si ya tienes infraestructura de producción que acepta webhooks entrantes de otros proveedores (Stripe, GitHub, Twilio), añadir Elido al mismo receptor es de baja fricción.

Los costes ocultos de los webhooks#

Los webhooks suenan simples: el servidor envía POST, tú lo manejas. La superficie de implementación real es mayor.

Diagrama en capas de las cuatro compuertas que pasa cada entrega click.recorded entrante: verificacion de firma HMAC, la ventana de replay de 300 segundos, deduplicacion de idempotencia por el ID de entrega, y mantenerse disponible antes de devolver 2xx.

Verificación de firma#

Elido firma cada entrega de webhook con HMAC-SHA256. El formato de firma es v1=HMAC-SHA256(secret, "${unix_timestamp}.${body}"), entregado en el header X-Webhook-Signature. La marca de tiempo se envía por separado en X-Webhook-Timestamp. Ambos se producen en services/webhook-dispatcher/internal/signing/hmac.go.

Debes verificar esta firma antes de procesar el payload. Un receptor que omite la verificación procesará cualquier POST que alcance el endpoint, incluyendo solicitudes spoofeadas de cualquiera que descubra tu URL de webhook.

Aquí hay un handler mínimo de Express en TypeScript que verifica la firma antes de hacer nada con el payload:

import express, { Request, Response } from "express";
import crypto from "crypto";

const app = express();

// Use raw body middleware - JSON parsers consume the stream before you can hash it
app.use("/webhook", express.raw({ type: "application/json" }));

function verifySignature(
  secret: string,
  signature: string,
  timestamp: string,
  rawBody: Buffer,
): boolean {
  const message = `${timestamp}.${rawBody.toString("utf8")}`;
  const expected =
    "v1=" + crypto.createHmac("sha256", secret).update(message).digest("hex");
  // Use timingSafeEqual to prevent timing-based enumeration
  return crypto.timingSafeEqual(
    Buffer.from(signature, "utf8"),
    Buffer.from(expected, "utf8"),
  );
}

app.post("/webhook", (req: Request, res: Response) => {
  const signature = req.headers["x-webhook-signature"] as string;
  const timestamp = req.headers["x-webhook-timestamp"] as string;

  if (!signature || !timestamp) {
    return res.status(400).json({ error: "missing signature headers" });
  }

  // Reject payloads older than 5 minutes
  const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
  if (age > 300) {
    return res.status(400).json({ error: "payload too old" });
  }

  if (
    !verifySignature(
      process.env.WEBHOOK_SECRET!,
      signature,
      timestamp,
      req.body as Buffer,
    )
  ) {
    return res.status(401).json({ error: "invalid signature" });
  }

  const event = JSON.parse((req.body as Buffer).toString("utf8"));

  if (event.type === "click.recorded") {
    // Handle the click event
    console.log("click recorded:", event.data);
  }

  // Always return 2xx promptly - do heavy processing async
  return res.status(200).json({ received: true });
});

La ventana de replay#

La verificación de marca de tiempo en el ejemplo anterior aplica lo que la documentación de Elido llama una ventana de replay. Sin ella, un atacante que captura un solo payload firmado válido puede replayearlo indefinidamente - la firma sigue siendo válida para siempre porque se calcula a partir de una marca de tiempo fija. Con la verificación, un payload más viejo que cinco minutos es rechazado independientemente de si la firma es válida.

Configura la tolerancia a algo que tu infraestructura pueda manejar. Cinco minutos es el valor predeterminado convencional y coincide con lo que usa Stripe. Si tu receptor ocasionalmente se cae durante unos pocos minutos durante despliegues, esta ventana le da tiempo a volver y aún procesar entregas en vuelo.

Reintentos e idempotencia#

El webhook-dispatcher de Elido reintenta entregas fallidas en un horario de backoff: primer reintento a 1 minuto, segundo a 5 minutos, tercero a 15 minutos. El valor máximo de intentos por entrega es 3 por defecto, según se define en el schema webhook_deliveries. Después de 3 intentos fallidos la entrega se marca como permanentemente fallida y aparece en el dashboard de notificaciones.

Esto significa que tu receptor puede recibir el mismo evento más de una vez. Cualquier procesamiento que tenga efectos secundarios - escribir a una base de datos, enviar un email, actualizar un contador - necesita ser idempotente. El header X-Webhook-Delivery lleva un ID de entrega estable que puedes usar como clave de idempotencia.

// Before processing, check whether this delivery has already been handled
const deliveryId = req.headers["x-webhook-delivery"] as string;
const alreadyProcessed = await redis.get(`webhook:delivery:${deliveryId}`);
if (alreadyProcessed) {
  return res.status(200).json({ received: true, duplicate: true });
}
// Mark as processed with a TTL that covers the retry window
await redis.set(`webhook:delivery:${deliveryId}`, "1", "EX", 3600);

Tu endpoint debe ser altamente disponible#

La ventana de reintento es finita. Si tu receptor está caído más de aproximadamente 21 minutos (1 + 5 + 15), las entregas agotarán sus intentos y fallarán permanentemente. Para eventos donde la entrega garantizada importa - enriquecimiento de CRM, hooks de facturación - tu infraestructura de receptor necesita disponibilidad adecuada, no un servidor de hobbyista que ocasionalmente reinicia.

Este es el coste más subestimado de los webhooks para equipos nuevos en HTTP entrante. El polling se degrada con gracia: si el trabajo de polling falla, simplemente corre de nuevo en el siguiente intervalo y se pone al día. Un receptor de webhook que no está disponible pierde eventos permanentemente a menos que tengas una estrategia de reconciliación.

Los costes ocultos del polling#

El polling parece simple desde fuera. Los costes reales se acumulan en producción.

El retraso es la restricción definitoria. Un cron job corriendo cada cinco minutos significa que los datos de clics están hasta cinco minutos obsoletos. Para la mayoría de los casos de uso retrospectivos esto es aceptable; para cualquier cosa user-facing, no lo es. Acortar el intervalo ayuda pero no elimina el retraso, y los intervalos muy cortos (menos de un minuto) empiezan a parecer martilleo de API en lugar de polling.

Solicitudes desperdiciadas. La mayoría de los intervalos de polling devuelven los mismos datos que la solicitud anterior. Si estás haciendo polling a un enlace de poco tráfico cada minuto y los clics llegan a aproximadamente uno por hora, 59 de cada 60 solicitudes no devuelven nada nuevo. Estas solicitudes aún cuentan contra tu límite de tasa de API.

Límites de tasa. La API de Elido aplica límites de tasa por workspace dimensionados por tier de facturación. Un trabajo de polling que corre frecuentemente a través de muchos enlaces en un workspace grande puede alcanzar estos límites, particularmente si otra automatización en el mismo workspace también está haciendo llamadas a la API. La API devuelve 429 Too Many Requests con un header X-RateLimit-Scope: workspace cuando esto sucede.

Paginación y eventos perdidos. El endpoint /clicks/recent usa paginación basada en cursor. Si haces polling en una ventana de tiempo fija - ?from=<last_poll>&to=<now> - y el volumen en esa ventana excede el tamaño de página, perderás eventos a menos que sigas next_cursor a través de todas las páginas. Una implementación de polling que no maneja la paginación silenciosamente perderá datos bajo carga.

El patrón híbrido#

Para la mayoría de los casos de uso en producción, la mejor respuesta no es una u otra.

Diagrama de arquitectura hibrida: Elido empuja eventos de clic a un handler de webhook como ruta principal en tiempo real, mientras un polling nocturno de /summary y /timeseries reconcilia los totales en tu almacen y llena las brechas dejadas por la ventana de reintentos.

Usa webhooks como el camino primario para reacción en tiempo real: actualizaciones de CRM, contadores en vivo, workflows event-driven. La latencia es baja; la sobrecarga operacional es manejable si ya tienes infraestructura HTTPS entrante.

Usa polling como una pasada de reconciliación semanal o diaria: extrae la serie temporal completa de la semana anterior, compara los totales contra lo que tu handler de webhook registró, e identifica cualquier brecha. Esto captura entregas que agotaron su ventana de reintentos durante una caída, eventos que llegaron fuera de orden, y cualquier discrepancia entre tu estado local y la fuente de verdad de Elido.

La API de analítica está bien adaptada a este rol. El endpoint /summary devuelve totales agregados para un rango de fechas en una sola consulta; el endpoint /timeseries devuelve buckets diarios. Un trabajo de reconciliación que corre una vez por noche y compara los conteos de clics registrados de tu CRM contra el resumen de la API para la misma ventana puede sacar a la luz problemas de integridad de datos antes de que se conviertan en problemas user-facing.

Un cron de polling en Python#

Para equipos que quieren empezar con polling y graduarse a webhooks más tarde, aquí hay una implementación mínima usando la librería schedule que llama a /clicks/recent en un intervalo de cinco minutos:

import schedule
import time
import requests
import os

API_BASE = "https://api.elido.app/v1/analytics"
WORKSPACE_ID = os.environ["ELIDO_WORKSPACE_ID"]
API_KEY = os.environ["ELIDO_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

# Track the cursor across poll intervals so we only fetch new clicks
_cursor = None

def poll_recent_clicks():
    global _cursor
    params = {"limit": 100}
    if _cursor:
        params["cursor"] = _cursor

    while True:
        resp = requests.get(
            f"{API_BASE}/workspaces/{WORKSPACE_ID}/clicks/recent",
            headers=HEADERS,
            params=params,
            timeout=10,
        )
        resp.raise_for_status()
        body = resp.json()

        items = body.get("items", [])
        for click in items:
            process_click(click)

        next_cursor = body.get("next_cursor")
        if not next_cursor:
            # Persist the current cursor for the next run
            if items:
                _cursor = None  # reset: next poll fetches from now
            break
        params["cursor"] = next_cursor

def process_click(click: dict):
    # Replace with your actual processing logic
    print(f"click: link={click['link_id']} country={click.get('country_code')}")

schedule.every(5).minutes.do(poll_recent_clicks)

if __name__ == "__main__":
    poll_recent_clicks()  # run once on startup to catch up
    while True:
        schedule.run_pending()
        time.sleep(10)

En un despliegue de producción, reemplaza el print con tu sink real - una escritura a base de datos, una llamada a la API del CRM, una publicación de cola de mensajes - y añade manejo de errores con backoff exponencial alrededor de la llamada requests.get.

Filtrado de bots y lo que significa para tu integración#

Un detalle que afecta a ambos patrones: el servicio edge-redirect de Elido filtra clics de bots antes de emitir eventos de clic a la pipeline de procesamiento. Las solicitudes de Googlebot, Bingbot, Slackbot, monitores de uptime, curl, librerías de scripting, y User-Agents vacíos no producen eventos click.recorded y no aparecen en los resultados de la API de analítica.

Esto importa porque significa que tu handler de webhook o trabajo de polling está trabajando con conteos de redirecciones humanas, no conteos de solicitudes HTTP crudas. Si estás correlacionando datos de clics de Elido contra métricas server-side - los logs del servidor de tu aplicación, los logs de acceso de una CDN - espera que los números de Elido sean menores. La discrepancia no es un bug; es el filtro de bots eliminando ruido antes de que te llegue.

Para más detalle sobre lo que cubre el filtro de bots y cómo el scorer de sospecha marca tráfico fronterizo, la guía de analítica tiene un desglose completo. Para las propiedades de seguridad del esquema de firma de webhook - incluyendo el formato HMAC, el binding de marca de tiempo, y lo que previene - ve la checklist de seguridad.


La página de precios tiene el desglose de qué tiers de plan incluyen endpoints de webhook y a qué topes de volumen de entrega.

Relacionado en el blog#

Prueba Elido

Pega una URL, obtén un enlace corto

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

Gratis, sin registro · 2 por día

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
click tracking
webhooks
url shortener api
link analytics
api integration
event-driven
polling
real-time analytics

Seguir leyendo