DEV Community

Cover image for Webhooks vs Polling: ¿Qué Patrón de Integración API es Mejor?
Roobia
Roobia

Posted on • Originally published at apidog.com

Webhooks vs Polling: ¿Qué Patrón de Integración API es Mejor?

En resumen: El sondeo busca actualizaciones periódicamente (sencillo pero ineficiente). Los webhooks envían actualizaciones en tiempo real (eficiente pero complejo). Usa el sondeo para verificaciones infrecuentes, y los webhooks para actualizaciones en tiempo real. La Modern PetstoreAPI soporta ambos patrones con entrega confiable de webhooks.

Prueba Apidog hoy

Comprendiendo la Diferencia

Sondeo (Polling): El cliente pregunta "¿Hay actualizaciones?" repetidamente. Webhooks: El servidor dice "¡Aquí hay una actualización!" cuando algo sucede.

Analogía:

  • Sondeo = Revisar tu buzón cada hora
  • Webhooks = El cartero toca el timbre cuando llega el correo

Sondeo: Cómo Funciona

El cliente realiza solicitudes periódicas para verificar cambios.

// Sondea cada 30 segundos
setInterval(async () => {
  const response = await fetch('https://petstoreapi.com/api/v1/orders/123');
  const order = await response.json();

  if (order.status === 'completed') {
    console.log('Order completed!', order);
    clearInterval(pollInterval);
  }
}, 30000);

Patrones de sondeo:

Sondeo simple:

GET /api/v1/orders/123
# Devuelve el estado actual del pedido

Sondeo condicional (ETag):

GET /api/v1/orders/123
If-None-Match: "abc123"

# Devuelve 304 Not Modified si no hay cambios
# Devuelve 200 con nuevos datos si hay cambios

Sondeo basado en "desde":

GET /api/v1/orders/123/events?since=1710331200
# Devuelve eventos desde la marca de tiempo

Webhooks: Cómo Funcionan

El servidor envía una solicitud HTTP POST a tu endpoint cuando ocurren eventos.

Flujo de configuración:

// 1. Registra el endpoint del webhook
POST /api/v1/webhooks
{
  "url": "https://myapp.com/webhooks/petstore",
  "events": ["order.created", "order.completed"],
  "secret": "whsec_abc123"
}

// 2. El servidor envía el webhook cuando ocurre el evento
POST https://myapp.com/webhooks/petstore
{
  "id": "evt_123",
  "type": "order.completed",
  "created": 1710331200,
  "data": {
    "orderId": "123",
    "status": "completed",
    "completedAt": "2024-01-01T12:00:00Z"
  }
}

// 3. Verifica y procesa el webhook
// Responde con 200 OK

Cuándo Usar el Sondeo

Bueno para:

  • Verificaciones infrecuentes (una vez por hora)
  • Un pequeño número de recursos
  • Implementaciones simples
  • Cuando controlas el cliente
  • Pruebas y depuración

Ejemplos:

  • Verificar el estado de informes diarios
  • Sincronizar contactos cada pocos minutos
  • Monitorizar la salud del servidor
  • Verificar el estado de pago (infrecuente)

El sondeo está bien cuando:

  • Las actualizaciones son raras
  • Un ligero retraso es aceptable
  • Quieres una implementación simple
  • El recurso es pequeño

Cuándo Usar Webhooks

Bueno para:

  • Actualizaciones en tiempo real
  • Muchos recursos a monitorizar
  • Eventos sensibles al tiempo
  • Integraciones de terceros
  • Actualizaciones de alta frecuencia

Ejemplos:

  • Confirmaciones de pago
  • Mensajes de chat
  • Alertas de precios de acciones
  • Cambios de estado de pedidos
  • Notificaciones de CI/CD

Los webhooks son mejores cuando:

  • Las actualizaciones necesitan ser inmediatas
  • El sondeo sería ineficiente
  • Muchos clientes monitorizando el mismo recurso
  • Quieres reducir la carga del servidor

Tabla Comparativa

Factor Sondeo Webhooks
Latencia Hasta el intervalo de sondeo Tiempo real
Carga del servidor Alta (muchas solicitudes vacías) Baja (solo eventos reales)
Complejidad Simple Complejo
Fiabilidad Alta (el cliente controla el reintento) Media (necesita lógica de reintento)
Configuración Ninguna Registro de endpoint
Problemas de firewall Ninguno (solo saliente) Puede necesitar incluir en lista blanca
Costo Mayor (más solicitudes) Menor (menos solicitudes)
Mejor para Verificaciones infrecuentes Actualizaciones en tiempo real

Implementando el Sondeo

Sondeo Básico

async function pollOrderStatus(orderId, callback) {
  let lastStatus = null;

  const poll = async () => {
    try {
      const response = await fetch(`https://petstoreapi.com/api/v1/orders/${orderId}`);
      const order = await response.json();

      // Solo llamar al callback si el estado ha cambiado
      if (order.status !== lastStatus) {
        lastStatus = order.status;
        callback(order);
      }

      // Detener el sondeo si el estado es terminal
      if (['completed', 'cancelled'].includes(order.status)) {
        return;
      }

      // Continuar sondeando
      setTimeout(poll, 5000);
    } catch (error) {
      console.error('Polling error:', error);
      setTimeout(poll, 30000); // Esperar más en caso de error
    }
  };

  poll();
}

// Uso
pollOrderStatus('order-123', (order) => {
  console.log(`Order status: ${order.status}`);
});

Sondeo Inteligente (Retroceso Exponencial)

async function smartPoll(url, callback, options = {}) {
  const {
    maxRetries = 10,
    initialInterval = 1000,
    maxInterval = 60000,
    stopCondition = () => false
  } = options;

  let retries = 0;
  let interval = initialInterval;
  let lastData = null;

  const poll = async () => {
    try {
      const response = await fetch(url);
      const data = await response.json();

      // Llamar al callback si los datos cambiaron
      if (JSON.stringify(data) !== JSON.stringify(lastData)) {
        lastData = data;
        callback(data);
      }

      // Detener si se cumple la condición
      if (stopCondition(data)) {
        return;
      }

      // Reiniciar el intervalo en solicitudes exitosas
      interval = initialInterval;

    } catch (error) {
      retries++;
      if (retries >= maxRetries) {
        throw new Error('Max retries exceeded');
      }
    }

    // Programar la siguiente consulta con retroceso exponencial
    setTimeout(poll, interval);
    interval = Math.min(interval * 2, maxInterval);
  };

  poll();
}

// Uso: Sondea un pedido hasta que se complete
smartPoll('https://petstoreapi.com/api/v1/orders/123',
  (order) => console.log('Order:', order),
  {
    stopCondition: (order) => ['completed', 'cancelled'].includes(order.status),
    initialInterval: 2000,
    maxInterval: 30000
  }
);

Sondeo con ETag

async function pollWithEtag(url, callback) {
  let etag = null;

  const poll = async () => {
    const headers = {};
    if (etag) {
      headers['If-None-Match'] = etag;
    }

    const response = await fetch(url, { headers });

    if (response.status === 304) {
      // No modificado, continuar sondeando
      setTimeout(poll, 30000);
      return;
    }

    const data = await response.json();
    etag = response.headers.get('etag');

    callback(data);
    setTimeout(poll, 30000);
  };

  poll();
}

Implementando Webhooks

Registrando Webhooks

// Registrar el endpoint del webhook
async function registerWebhook(url, events) {
  const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`
    },
    body: JSON.stringify({
      url,
      events,
      secret: generateSecret()
    })
  });

  return response.json();
}

function generateSecret() {
  return 'whsec_' + crypto.randomBytes(32).toString('hex');
}

Recibiendo Webhooks

const express = require('express');
const crypto = require('crypto');
const app = express();

// Analizador de cuerpo crudo para la verificación de firma
app.use('/webhooks', express.raw({ type: 'application/json' }));

app.post('/webhooks/petstore', async (req, res) => {
  const signature = req.headers['x-petstore-signature'];
  const body = req.body;

  // Verificar firma
  const isValid = verifySignature(body, signature, process.env.WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(body.toString());

  // Procesar evento
  switch (event.type) {
    case 'order.created':
      await handleOrderCreated(event.data);
      break;
    case 'order.completed':
      await handleOrderCompleted(event.data);
      break;
    case 'order.cancelled':
      await handleOrderCancelled(event.data);
      break;
  }

  // Confirmar recepción
  res.status(200).json({ received: true });
});

function verifySignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

Probando Webhooks Localmente

# Usa ngrok para exponer el endpoint local
ngrok http 3000

# Registra la URL de ngrok como endpoint de webhook
curl -X POST https://petstoreapi.com/api/v1/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "url": "https://abc123.ngrok.io/webhooks/petstore",
    "events": ["order.created", "order.completed"]
  }'

Entrega Confiable de Webhooks

Los webhooks pueden fallar. Implementa lógica de reintento.

Lado del Remitente (Servidor)

// Encola webhooks para su entrega
const webhookQueue = [];

async function sendWebhook(event) {
  const webhooks = await db.webhooks.findMany({
    where: { events: { contains: event.type } }
  });

  for (const webhook of webhooks) {
    webhookQueue.push({
      webhook,
      event,
      attempts: 0,
      nextAttempt: Date.now()
    });
  }

  processQueue();
}

async function processQueue() {
  const now = Date.now();

  for (const item of webhookQueue) {
    if (item.nextAttempt > now) continue;

    try {
      await deliverWebhook(item);
      // Eliminar de la cola al tener éxito
      webhookQueue.splice(webhookQueue.indexOf(item), 1);
    } catch (error) {
      // Programar reintento con retroceso exponencial
      item.attempts++;
      item.nextAttempt = now + getBackoff(item.attempts);

      if (item.attempts >= 5) {
        // Marcar como fallido después de 5 intentos
        await markWebhookFailed(item);
        webhookQueue.splice(webhookQueue.indexOf(item), 1);
      }
    }
  }

  setTimeout(processQueue, 5000);
}

function getBackoff(attempt) {
  // 1min, 5min, 15min, 1hr, 4hr
  const delays = [60000, 300000, 900000, 3600000, 14400000];
  return delays[attempt - 1] || delays[delays.length - 1];
}

async function deliverWebhook({ webhook, event }) {
  const signature = generateSignature(event, webhook.secret);

  const response = await fetch(webhook.url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Petstore-Signature': signature,
      'X-Petstore-Event': event.type
    },
    body: JSON.stringify(event),
    timeout: 10000
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
}

Lado del Receptor (Cliente)

// Manejo de webhook idempotente
const processedEvents = new Set();

app.post('/webhooks/petstore', async (req, res) => {
  const event = JSON.parse(req.body.toString());

  // Omitir si ya se procesó (idempotencia)
  if (processedEvents.has(event.id)) {
    return res.status(200).json({ received: true });
  }

  try {
    await processEvent(event);
    processedEvents.add(event.id);

    // Limpiar IDs de eventos antiguos (mantener los últimos 1000)
    if (processedEvents.size > 1000) {
      const arr = Array.from(processedEvents);
      arr.slice(0, arr.length - 1000).forEach(id => processedEvents.delete(id));
    }

    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook processing error:', error);
    // Devolver 5xx para activar el reintento
    res.status(500).json({ error: 'Processing failed' });
  }
});

async function processEvent(event) {
  // Procesar el evento
  switch (event.type) {
    case 'order.created':
      await handleOrderCreated(event.data);
      break;
    // ... manejar otros eventos
  }
}

Enfoque Híbrido

Usa tanto el sondeo como los webhooks para actualizaciones críticas.

class OrderMonitor {
  constructor(orderId, callback) {
    this.orderId = orderId;
    this.callback = callback;
    this.pollInterval = null;
  }

  async start() {
    // Comenzar con sondeo para retroalimentación inmediata
    this.startPolling();

    // Registrar webhook para actualización en tiempo real
    await this.registerWebhook();
  }

  startPolling() {
    this.pollInterval = setInterval(async () => {
      const order = await this.fetchOrder();
      this.callback(order);

      if (['completed', 'cancelled'].includes(order.status)) {
        this.stop();
      }
    }, 10000);
  }

  async registerWebhook() {
    const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${TOKEN}` },
      body: JSON.stringify({
        url: 'https://myapp.com/webhooks/petstore',
        events: [`order.${this.orderId}`],
        oneTime: true // Auto-eliminar después de la primera entrega
      })
    });

    this.webhookId = (await response.json()).id;
  }

  stop() {
    if (this.pollInterval) {
      clearInterval(this.pollInterval);
    }
    if (this.webhookId) {
      fetch(`https://petstoreapi.com/api/v1/webhooks/${this.webhookId}`, {
        method: 'DELETE'
      });
    }
  }
}

Preguntas Frecuentes

P: ¿Con qué frecuencia debo sondear? Depende de la urgencia. 30 segundos para casi tiempo real. 5 minutos para no urgentes. Equilibra la frescura con la carga del servidor.

P: ¿Qué pasa si mi endpoint de webhook está caído? Los buenos proveedores de webhooks reintentan con retroceso exponencial. Implementa la idempotencia para manejar entregas duplicadas.

P: ¿Cómo aseguro los webhooks? Verifica las firmas usando secretos compartidos. Usa solo HTTPS. Valida los datos del evento.

P: ¿Puedo usar webhooks para datos históricos? No. Los webhooks son solo para eventos nuevos. Usa el sondeo o las APIs por lotes para datos históricos.

P: ¿Debo usar sondeo o webhooks para aplicaciones móviles? El sondeo es más simple para móviles. Los webhooks requieren notificaciones push como intermediario.

P: ¿Cómo depuro problemas de webhook? Usa herramientas como webhook.site para pruebas. Registra todas las entregas de webhooks. Proporciona un historial de eventos de webhook en tu API.

Modern PetstoreAPI soporta tanto el sondeo como los webhooks. Consulta la guía de webhooks para detalles de implementación. Prueba las integraciones de webhook con Apidog.

Top comments (0)