webhooks · confiabilidad-api · eventos · estrategia-reintento · idempotencia
Diseñando Entrega Confiable de Webhooks para APIs Pequeñas: Una Guía Conservadora
Una guía práctica para desarrolladores independientes sobre elegir procesamiento síncrono vs asíncrono, implementar reintentos con backoff exponencial y usar claves de idempotencia para prevenir procesamiento duplicado de eventos.
Publicado:
El Problema del que Nadie Habla
Los webhooks parecen simples. Un proveedor envía un HTTP POST a tu endpoint, procesas el evento, devuelves 200. Listo. Pero en producción, esta simplicidad se desvanece. Los eventos se pierden. Las entregas duplicadas se acumulan. Tu endpoint supera el tiempo de espera y el proveedor reintenta hasta que quieres gritar. Para equipos pequeños sin infraestructura dedicada, estos problemas se multiplican rápidamente.
Esta guía cubre tres decisiones que separan integraciones de webhooks frágiles de confiables: procesamiento síncrono versus asíncrono, diseño de estrategia de reintento, y manejo idempotente de eventos. El consejo aquí es técnicamente conservador porque los fallos de webhook son costosos: pagos perdidos, envíos duplicados, estado corrupto.
Callbacks Síncronos vs Colas Asíncronas
La primera decisión arquitectónica es si procesar los payloads de webhook inmediatamente o diferirlos. La mayoría de los tutoriales muestran manejadores síncronos porque son más simples de escribir. Recibes la solicitud, haces el trabajo, respondes. Esto funciona hasta que no funciona.
La trampa síncrona: Los proveedores timeout las solicitudes después de 5 a 30 segundos dependiendo del servicio. Twilio usa 15 segundos para webhooks de voz y 5 segundos para Conversations. Si tu manejador tarda más—porque estás llamando APIs externas, escribiendo en bases de datos, o ejecutando lógica de negocio—el proveedor asume fallo e reintenta. Ahora tienes eventos duplicados acumulándose mientras tu endpoint aún procesa el primero.
El patrón asíncrono: Devuelve un estado 200 inmediatamente, luego pon en cola el evento para procesamiento en segundo plano. Este es el enfoque recomendado por Stripe, Twilio y múltiples equipos de ingeniería que aprendieron esto de la manera difícil.
// MAL: Procesamiento síncrono
app.post('/webhooks/stripe', async (req, res) => {
await processPaymentEvent(req.body); // Podría tomar 10+ segundos
res.status(200).send('OK');
});
// BIEN: Reconocimiento asíncrono
app.post('/webhooks/stripe', async (req, res) => {
await queue.add('process-webhook', req.body);
res.status(200).send('OK'); // Devolver inmediatamente
});
El compromiso es complejidad operacional. El procesamiento asíncrono requiere un sistema de colas, monitoreo de trabajos atascados, y una estrategia para reprocesar eventos fallidos. Pero los manejadores síncronos que timeout crean un problema peor: pérdida silenciosa de datos. Como descubrió el equipo de ingeniería de Stigg, las preguntas que necesitas responder son: ¿Qué pasa si estamos caídos y los webhooks no se procesan? ¿Qué pasa si el procesamiento falla silenciosamente? ¿Cómo monitoreamos los fallos apropiadamente? ¿Cómo reprocesamos eventos fallidos cuando arreglamos bugs? ¿Cómo escalamos sin perder datos?
Entendiendo el Comportamiento de Reintento del Proveedor
Diferentes proveedores implementan lógica de reintento de manera diferente. Entender estas diferencias es esencial para diseñar manejadores que funcionen correctamente entre servicios.
Stripe reintenta la entrega de eventos por hasta 3 días en modo producción con backoff exponencial. El modo prueba hace tres intentos de reintento durante unas pocas horas. Los intervalos siguen un patrón: 1 hora, 2 horas, 4 horas, 8 horas, y así sucesivamente. El dashboard de Stripe muestra los intentos de entrega y permite reintento manual.
Twilio usa un solo reintento en timeout por defecto, pero esto es configurable mediante overrides de conexión hasta 5 intentos. Los webhooks de voz tienen timeout de 15 segundos; los webhooks de Conversations timeout después de 5 segundos.
Shopify reintenta por hasta 48 horas con 19 intentos de reintento. Los intervalos comienzan en 10 segundos y aumentan a horas.
GitHub reintenta hasta 3 veces dentro de una ventana corta, con webhooks fallidos visibles en el log de entrega.
El patrón es claro: los proveedores reintentarán. Tu manejador debe estar preparado para múltiples entregas del mismo evento. Esto lleva a la segunda decisión crítica.
Implementando Backoff Exponencial
Si estás construyendo tu propio remitente de webhooks o lógica de reintento, el backoff exponencial es el enfoque estándar. El concepto es simple: espera progresivamente más tiempo entre intentos de reintento. Esto previene sobrecargar al receptor durante interrupciones y da tiempo a que fallos transitorios se resuelvan.
Una implementación básica se ve así:
async function sendWithRetry(url, payload, maxRetries = 5) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (response.ok) return true;
// Esperar antes de reintentar (backoff exponencial)
if (attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s, 16s
await new Promise(resolve => setTimeout(resolve, delay));
}
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
El compromiso es latencia versus confiabilidad. Los reintentos agresivos (intervalos cortos, muchos intentos) pueden sobrecargar a un receptor vulnerable. Los reintentos conservadores (intervalos largos, pocos intentos) arriesgan perder eventos durante interrupciones breves. Ajusta tu estrategia de reintento al comportamiento de tu proveedor. Si Stripe reintenta por 3 días, tu manejador debe esperar eventos llegando durante esa ventana.
Manejo Idempotente de Webhooks
El procesamiento duplicado de eventos no es un bug—es una característica de sistemas distribuidos. Las redes fallan. Los proveedores reintentan. Tu endpoint recibe el mismo evento múltiples veces. La idempotencia asegura que procesar un evento múltiples veces produce el mismo resultado que procesarlo una vez.
La regla de oro: Usa el ID del evento como clave, no el payload.
Cada proveedor importante incluye un identificador de evento en payloads de webhook. Stripe usa event.id. Twilio incluye encabezados I-Twilio-Idempotency-Token. GitHub proporciona encabezados X-GitHub-Delivery. Almacena estos identificadores en tu base de datos y verifícalos antes de procesar.
const processedEvents = new Set();
app.post('/webhooks', async (req, res) => {
const eventId = req.headers['x-event-id'] || req.body.id;
if (processedEvents.has(eventId)) {
console.log('Evento duplicado, omitiendo:', eventId);
return res.status(200).send('OK');
}
await processEvent(req.body);
processedEvents.add(eventId);
res.status(200).send('OK');
});
Para sistemas de producción, usa una base de datos en lugar de un Set en memoria. Verifica IDs de eventos existentes antes de procesar, y marca eventos como procesados después del manejo exitoso. Esto maneja reinicios, despliegues distribuidos, y procesos de larga duración.
Patrones comunes de idempotencia:
-
Operaciones upsert: Usa
INSERT ... ON CONFLICT UPDATEen PostgreSQL, oupserten MongoDB. Esto hace que el procesamiento repetido sea una operación vacía después del primer ejecución exitosa. -
Seguimiento de estado: Almacena el estado de procesamiento de eventos en tu base de datos. Verifica el estado antes de procesar, actualiza después de completar.
-
Tablas de deduplicación: Crea una tabla separada para IDs de eventos procesados con restricción única. Inserta el ID primero; si falla por duplicado, omite el procesamiento.
El Patrón Outbox Transaccional
Para equipos construyendo APIs orientadas a eventos, el patrón outbox transaccional proporciona un fundamento confiable. En lugar de enviar webhooks directamente desde la lógica de aplicación, escribes eventos en una tabla outbox dentro de la misma transacción de base de datos que tu lógica de negocio.
-- Outbox universal para todos los eventos
CREATE TABLE webhook_outbox (
id SERIAL PRIMARY KEY,
event_type VARCHAR(255) NOT NULL,
aggregate_id VARCHAR(255) NOT NULL,
payload JSONB NOT NULL,
status VARCHAR(50) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP
);
El flujo funciona así:
- La lógica de aplicación ejecuta y escribe en la tabla outbox dentro de la misma transacción
- Un worker en segundo plano consulta eventos pendientes
- El worker envía webhooks con lógica de reintento
- El worker actualiza el estado a ‘processed’ en éxito, ‘failed’ en fallo permanente
Este patrón desacopla la generación de eventos de la entrega de eventos. Si tu aplicación falla después de escribir en el outbox pero antes de enviar el webhook, el evento se preserva. El worker en segundo plano eventualmente lo procesará. Este es el enfoque recomendado por la discusión de integración Supabase-WordPress para manejar confiabilidad de webhooks.
El compromiso es tablas de base de datos adicionales y un worker en segundo plano. Pero para equipos pequeños, esto es más barato que depurar eventos perdidos o procesamiento duplicado en producción.
FAQ
P: ¿Puedo usar solo manejadores síncronos si mi procesamiento es rápido?
R: Si tu manejador consistentemente completa en menos de 5 segundos y no llama servicios externos, el procesamiento síncrono es aceptable. Pero “rápido hoy” no garantiza “rápido mañana”. A medida que tu aplicación crece, los manejadores síncronos se convierten en pasivo. Planifica para asíncrono desde el inicio.
P: ¿Cómo manejo la verificación de firma de webhook con procesamiento asíncrono?
R: Verifica firmas antes de poner en cola el evento. Esto asegura que no estás procesando payloads fraudulentos. Stripe, Twilio, y otros proveedores incluyen mecanismos de firma (HMAC-SHA1 para Twilio, verificación de firma para Stripe) que deben verificarse inmediatamente al recibir.
P: ¿Qué pasa si mi cola se llena o los workers fallan?
R: Monitorea la profundidad de cola y la salud del worker. Configura alertas para trabajos atascados. Implementa colas de letra muerta para eventos que fallan después de reintentos máximos. El issue de webhooks de GoHighLevel destaca que sin modelado de reintento apropiado, los eventos pueden perderse permanentemente.
P: ¿Debería usar un servicio gestionado de webhooks como Hookdeck o AWS SNS?
R: Los servicios gestionados manejan lógica de reintento, seguimiento de entrega, y verificación de firma por ti. Reducen carga operacional pero añaden costo y dependencia. Para APIs pequeñas con bajo volumen de eventos, construir tu propia lógica de reintento e idempotencia puede ser más simple y barato.
P: ¿Cómo pruebo la confiabilidad de webhooks sin un entorno de producción?
R: Usa herramientas como Stripe CLI para pruebas locales. Simula reintentos de proveedor enviando eventos duplicados con el mismo ID. Prueba tu lógica de idempotencia procesando el mismo evento múltiples veces. Verifica que tu manejador devuelva 200 rápidamente incluso cuando el procesamiento en segundo plano es lento.
Conclusión
La entrega confiable de webhooks requiere tres disciplinas: devolver 200 rápidamente, procesar eventos asíncronamente, y manejar duplicados idempotentemente. Los proveedores reintentarán. Tu trabajo es hacer que esos reintentos sean seguros. El patrón outbox transaccional añade confiabilidad al costo de complejidad operacional. Para equipos pequeños, este compromiso usualmente vale la pena—eventos perdidos son más caros que workers en segundo plano.
Comienza simple. Añade procesamiento asíncrono y verificaciones de idempotencia temprano. Monitorea tus endpoints de webhook. Cuando los proveedores reintenten y lleguen duplicados—y llegarán—estarás listo.
Fuentes
- https://hookdeck.com/webhooks/platforms/twilio-webhooks-features-and-best-practices-guide
- https://docs.stripe.com/webhooks
- https://www.stigg.io/blog-posts/best-practices-i-wish-we-knew-when-integrating-stripe-webhooks
- https://www.twilio.com/en-us/blog/insights/whats-webhook
- https://github.com/GoHighLevel/highlevel-api-docs/issues/257
- https://github.com/hookdeck/webhook-skills/blob/main/skills/webhook-handler-patterns/references/retry-logic.md
- https://github.com/AI-agents-incubator/supabase-wordpress/issues/11
