Ir al contenido

Webhooks e integraciones

Reintentos con exponential backoff

Diseña reintentos de webhooks con espera exponencial, jitter y límites claros para recuperarte de fallos sin sobrecargar al receptor.

Por Equipo AltokePayPublicado Actualizado 9 min de lectura

Una entrega de webhook puede fallar aunque emisor y receptor estén bien diseñados. Una actualización despliega durante unos segundos, una dependencia responde lentamente o la red interrumpe una conexión. Reintentar permite recuperarse de fallos transitorios, pero repetir de inmediato y sin límites puede convertir una incidencia pequeña en una avalancha.

El exponential backoff, o espera exponencial, incrementa la demora entre intentos. Suele combinarse con jitter, una variación aleatoria que evita que miles de entregas vuelvan a coincidir exactamente. No existe una secuencia universal: cada proveedor define códigos reintentables, tiempos, cantidad de intentos, cabeceras reconocidas y periodo de conservación. El consumidor debe leer ese contrato; si implementa el emisor, debe publicar uno claro.

Por qué reintentar inmediatamente empeora el fallo

Supongamos que un receptor está saturado y cien emisores reciben error al mismo tiempo. Si todos repiten cada segundo, el servicio no obtiene espacio para recuperarse. Incluso después de mejorar, recibe otra ráfaga sincronizada. Este comportamiento se conoce como efecto de estampida.

Una progresión básica puede expresarse así:

demora = mínimo(límite, base × 2^intento)

Con base de dos segundos, la secuencia teórica sería 2, 4, 8, 16 y 32 segundos. El límite impide que la demora crezca indefinidamente. Sin embargo, todos los trabajadores que empezaron juntos seguirían la misma secuencia. El jitter rompe esa sincronía.

Una variante llamada full jitter elige una demora aleatoria entre cero y el máximo calculado:

demora = aleatorio(0, mínimo(límite, base × 2^intento))

No copies valores de ejemplo como política productiva. La base, el límite y el horizonte total dependen de la urgencia del evento, capacidad del receptor, volumen, acuerdos operativos y posibilidad de recuperación manual.

Qué fallos son reintentables

Un timeout o un 503 Service Unavailable suele indicar una condición temporal, pero el contrato puede matizarlo. Un 400 Bad Request normalmente señala que repetir la misma carga no cambiará el resultado. Un 401 podría deberse a una credencial mal configurada y requerir intervención, no miles de intentos idénticos.

El RFC 9110 describe la semántica de los códigos HTTP, incluido 503, y define Retry-After para comunicar cuánto tiempo esperar en ciertos casos. Aun así, un sistema debe especificar si respeta esa cabecera, cómo combina su valor con el backoff y qué límites aplica a entradas extremas o inválidas.

Una clasificación inicial puede ser:

Resultado Decisión orientativa
Conexión interrumpida o timeout Reintentar con backoff
408, 429, 502, 503, 504 Evaluar como transitorio según contrato
Otros 4xx No repetir automáticamente salvo indicación
Respuesta de éxito acordada Marcar la entrega como aceptada
Código desconocido Aplicar política documentada y observar

Esta tabla no sustituye la documentación del proveedor. Además, un timeout es ambiguo: el receptor pudo completar la acción y perderse solo la respuesta. Por eso todo reintento exige un consumidor idempotente.

Ejemplo en PHP sin dependencias

El siguiente código representa un emisor hipotético. Usa cURL, un secreto de entorno solo como token placeholder y full jitter. La autenticación real, los códigos aceptados y los parámetros deben adaptarse al contrato correspondiente.

<?php

function deliverWebhook(string $jsonBody): bool
{
    $token = getenv('GENERIC_WEBHOOK_TOKEN');
    if ($token === false || $token === '') {
        throw new RuntimeException('Missing webhook token');
    }

    $trustedDestination = 'https://hooks.example.test/events';
    $maximumAttempts = 6;
    $baseDelayMs = 500;
    $maximumDelayMs = 30_000;
    $retryableStatusCodes = [408, 429, 502, 503, 504];

    for ($attempt = 0; $attempt < $maximumAttempts; $attempt++) {
        $handle = curl_init($trustedDestination);
        curl_setopt_array($handle, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $jsonBody,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $token,
            ],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
            CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS,
            CURLOPT_CONNECTTIMEOUT => 3,
            CURLOPT_TIMEOUT => 10,
        ]);

        curl_exec($handle);
        $statusCode = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
        $transportFailed = curl_errno($handle) !== 0;
        curl_close($handle);

        if (!$transportFailed && $statusCode >= 200 && $statusCode < 300) {
            return true;
        }

        $shouldRetry = $transportFailed || in_array(
            $statusCode,
            $retryableStatusCodes,
            true
        );

        if (!$shouldRetry || $attempt === $maximumAttempts - 1) {
            return false;
        }

        $exponentialLimit = min(
            $maximumDelayMs,
            $baseDelayMs * (2 ** $attempt)
        );
        usleep(random_int(0, $exponentialLimit) * 1000);
    }

    return false;
}

El destino del ejemplo es fijo y HTTPS. Si una plataforma permite destinos configurados por terceros, debe validarlos con una política contra SSRF que bloquee redes internas, esquemas no autorizados, redirecciones y resoluciones DNS inseguras antes de adjuntar credenciales. La documentación de PHP explica las opciones y funciones de cURL. El ejemplo mantiene el proceso bloqueado durante la espera, lo cual es fácil de leer pero poco conveniente a escala. Un diseño operativo suele persistir el próximo intento y permitir que un trabajador atienda otras entregas mientras llega ese momento.

Persistir antes de enviar

Si la entrega solo existe en memoria, reiniciar el proceso elimina su historial. Un emisor más resistente crea un registro con identidad del evento, destino, número de intento, próximo momento elegible y estado. Un trabajador reclama entregas vencidas, realiza la solicitud y actualiza el resultado de forma atómica.

No guardes secretos dentro de cada registro. Conserva una referencia segura a la configuración del destino. Limita también lo registrado de respuestas: cuerpos completos pueden contener datos sensibles o crecer sin control.

Separar evento de intento facilita responder preguntas distintas:

  • ¿qué ocurrió en el sistema de origen?;
  • ¿a qué destinos debía notificarse?;
  • ¿cuántas solicitudes se hicieron?;
  • ¿qué resultado tuvo cada una?;
  • ¿cuándo será el próximo intento?;
  • ¿por qué terminó la entrega?

Una cola de mensajes puede ayudar, pero no elimina estas decisiones. La cola también tiene semántica de confirmación, visibilidad, repetición y mensajes fallidos que deben entenderse.

Límite, vencimiento y cola de fallos

Reintentar para siempre oculta problemas permanentes y consume recursos. Define un límite por cantidad, edad total o ambos. Cuando se supera, mueve la entrega a un estado terminal visible, a veces llamado cola de mensajes fallidos. Ese estado necesita responsables, métricas y una vía segura de reproducción.

Reproducir no significa crear una identidad nueva para engañar al receptor. Idealmente conserva el identificador del evento y genera un nuevo intento auditable. Así, la deduplicación sigue funcionando si la primera solicitud sí produjo efectos.

El vencimiento también depende del dominio. Una notificación operacional de hace varios días podría requerir consulta del estado actual antes de aplicarse. El sistema no debe asumir que “eventualmente entregado” equivale siempre a “todavía válido”.

Presupuesto de reintentos y protección del receptor

Además del backoff por entrega, aplica límites globales. Si un destino falla durante una hora, millones de eventos podrían quedar programados. Controles como concurrencia máxima por destino, circuit breaker y presupuesto de reintentos evitan que un solo receptor consuma toda la capacidad.

El circuit breaker pausa temporalmente nuevas solicitudes tras una tasa elevada de fallos y prueba la recuperación con tráfico limitado. No reemplaza el backoff: coordina el comportamiento agregado, mientras el backoff decide cuándo vuelve a ser elegible cada entrega.

Para evitar retrasar eventos sanos detrás de uno defectuoso, define particiones o colas por destino cuando el volumen lo justifique. Mantén límites de memoria, conexiones y respuesta. El objetivo es recuperarse, no ganar una carrera contra un servicio caído.

Observabilidad que permite actuar

Registra el identificador del evento, destino lógico, intento, latencia, categoría del resultado y próxima fecha. Evita credenciales y datos sensibles. Mide:

  • porcentaje aceptado en el primer intento;
  • cantidad y edad de entregas pendientes;
  • reintentos por destino y código;
  • tiempo hasta aceptación;
  • entregas agotadas;
  • tamaño de la cola más antigua.

Una tasa alta de 400 apunta a un problema de contrato, no a falta de reintentos. Muchos timeouts con acciones duplicadas apuntan a idempotencia deficiente. Una cola que crece con 503 puede exigir reducir presión y coordinar con el receptor.

Reintentar sin repetir la acción

Los reintentos mejoran la probabilidad de entrega, pero hacen inevitable que una misma notificación llegue más de una vez. El emisor no puede saber siempre si el receptor procesó la primera solicitud. Diseña desde el principio con una identidad estable y consulta la guía para evitar webhooks duplicados mediante idempotencia.

La espera exponencial tampoco autentica mensajes; si el contrato utiliza firmas, la verificación HMAC-SHA256 es una preocupación separada. Un flujo robusto combina decisiones explícitas: qué se reintenta, cuánto tiempo, con qué dispersión, cómo se persiste y cuándo interviene una persona. Ninguna fórmula garantiza entrega o disponibilidad absolutas, pero una política limitada y observable evita que el mecanismo de recuperación se convierta en otra fuente de fallos.

Lecturas relacionadas