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

Ashley Innocent

Ashley Innocent

20 March 2026

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

Apidog para empresas

Despliegue local

SSO & RBAC

Conforme con SOC 2

Explorar Apidog Enterprise

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.

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: 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:

Ejemplos:

El sondeo está bien cuando:

Cuándo Usar Webhooks

Bueno para:

Ejemplos:

Los webhooks son mejores cuando:

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.

Practica el diseño de API en Apidog

Descubre una forma más fácil de construir y usar APIs