Fiabilidad de webhooks: deja de perder eventos de Stripe, GitHub y Slack
La fiabilidad de webhooks evita eventos perdidos o duplicados de Stripe, GitHub y Slack añadiendo firmas, idempotencia, reintentos y manejo dead-letter.

Por qué fallan los manejadores de webhooks en la vida real
Un webhook es un callback: un sistema le envía a tu servidor una petición HTTP cuando ocurre algo, como un pago, un push o un nuevo mensaje.
En teoría suena bien. En producción la fiabilidad se rompe porque las redes son desordenadas y los proveedores se protegen con reintentos. El mismo evento puede llegar dos veces, llegar tarde o parecer que nunca llegó.
Los equipos suelen enfrentarse a unos modos de fallo recurrentes:
- Eventos perdidos: tu endpoint se quedó sin respuesta, se bloqueó o estuvo brevemente caído.
- Duplicados: el proveedor reintentó y procesaste el mismo evento otra vez.
- Entrega fuera de orden: el evento B llega antes que el A, aunque A sucediera primero.
- Procesamiento parcial: escribiste en la base de datos, luego fallaste antes de responder y te reintentaron.
La mayoría de los proveedores solo prometen una entrega “al menos una vez”, no “exactamente una vez”. Intentarán entregar, pero no pueden garantizar tiempos perfectos, orden perfecto ni una única entrega.
Así que el objetivo no es hacer que los webhooks se comporten perfectamente. El objetivo es que tus resultados sean correctos incluso cuando las peticiones lleguen dos veces, lleguen tarde o lleguen fuera de orden. El resto de esta guía se centra en cuatro defensas que cubren la mayoría de fallos reales: idempotencia, verificación de firma, reintentos sensatos y una ruta dead-letter para que los fallos sean visibles en vez de silenciosos.
Qué harán Stripe, GitHub y Slack con tu endpoint
Los proveedores de webhooks son educados, pero no pacientes. Envían un evento, esperan un corto tiempo y si tu endpoint no responde como esperan, lo intentan de nuevo. Eso es comportamiento normal.
Asume que esto ocurrirá tarde o temprano:
- Timeouts: tu endpoint tarda demasiado, así que lo tratan como fallido.
- Reintentos: reenvían el mismo evento, a veces varias veces.
- Picos: un día tranquilo se convierte en 200 eventos en un minuto.
- Fallos temporales: tu servidor devuelve un 500, un deploy reinicia un worker o el DNS tiene un tropiezo.
- Retrasos en la entrega: los eventos llegan minutos más tarde de lo esperado.
La entrega duplicada sorprende a muchos equipos. Incluso si tu código hizo lo correcto, el proveedor puede no saberlo. Si tu manejador se queda sin tiempo, devuelve un non-2xx o cierra la conexión temprano, el mismo evento puede volver. Si tratas cada entrega como nueva, puedes cobrar doble, hacer dos upgrades, enviar emails duplicados o crear registros duplicados.
El orden tampoco está garantizado. Podrías ver "subscription.updated" antes que "subscription.created", o una edición de mensaje de Slack antes de la creación original, dependiendo de reintentos y rutas de red. Si tu lógica asume una secuencia limpia, puedes sobrescribir datos más nuevos con datos antiguos.
Esto empeora cuando tu manejador depende de trabajo lento aguas abajo como una escritura en la base de datos, envío de email o llamada a otra API. Un fallo realista se ve así: tu código espera 8 segundos a un proveedor de correo, el remitente del webhook agota el tiempo a los 5 segundos, reintenta y ahora dos peticiones compiten por actualizar el mismo registro.
Un buen manejador trata los webhooks como entregas poco fiables: acepta rápido, verifica, desduplica y procesa de forma controlada.
Idempotencia: la única solución que previene el doble procesamiento
Idempotencia significa esto: si el mismo evento webhook llega a tu servidor dos veces (o diez), tu sistema termina en el mismo estado que si lo hubieras manejado una sola vez. Eso importa porque los reintentos son normales.
En la práctica, idempotencia es desduplicar con memoria. Cuando llega un evento, compruebas si ya lo procesaste. Si sí, devuelves éxito y no haces nada más. Si no, lo procesas y registras que lo hiciste.
Qué necesitas para desduplicar
No necesitas mucho, pero sí algo estable:
- ID de evento del proveedor (lo mejor cuando está disponible)
- Nombre del proveedor (Stripe vs GitHub vs Slack)
- Cuándo lo viste por primera vez (útil para limpieza y depuración)
- Estado de procesamiento (recibido, procesado, fallido)
Guarda esto en algún sitio duradero. Una tabla de base de datos es el valor por defecto más seguro. Un caché con TTL puede servir para eventos de bajo riesgo, pero puede olvidarse durante reinicios o evictions. Para dinero o cambios de acceso, trata el registro de dedupe como parte de tus datos.
¿Cuánto tiempo deberías guardar las claves? Manténlas más tiempo que la ventana de reintentos del proveedor y más tiempo que tus propios reintentos diferidos. Muchos equipos conservan entre 7 y 30 días y luego expiran registros antiguos.
Efectos secundarios a proteger
La idempotencia protege tus acciones de mayor riesgo: cobrar dos veces, enviar emails duplicados, actualizar un rol dos veces, emitir un reembolso doble o crear tickets duplicados. Si solo haces una mejora de fiabilidad esta semana, haz esta.
Verificación de firma sin trampas
La verificación de firmas impide que tráfico aleatorio de Internet finja ser Stripe, GitHub o Slack. Sin ella, cualquiera puede golpear tu URL de webhook y provocar acciones como "marcar factura pagada" o "invitar usuario al workspace." Los eventos suplantados pueden parecer lo suficientemente válidos para pasar verificaciones JSON básicas.
Lo que normalmente verificas es lo mismo en todos los proveedores: el cuerpo bruto de la petición (bytes exactos), una marca de tiempo (para bloquear replays), un secreto compartido y el algoritmo esperado (a menudo un HMAC). Si cualquiera de esas entradas cambia aunque sea un poco, la firma no coincidirá.
La trampa que rompe integraciones reales con más frecuencia: parsear JSON antes de verificar. Muchos frameworks parsean y re-serializan el body, lo cual cambia espacios en blanco u orden de claves. Tu código entonces verifica una cadena diferente a la que firmó el proveedor y rechazas eventos reales.
Otros fallos comunes:
- Usar el secreto equivocado (test vs producción, o el secreto de otro endpoint).
- Ignorar la tolerancia de la marca de tiempo y luego rechazar eventos válidos cuando el reloj del servidor deriva.
- Verificar el header equivocado (algunos proveedores envían múltiples versiones de la firma).
- Devolver 200 incluso cuando la verificación falla, lo que complica la depuración.
Manejar errores de forma segura es sencillo: si la verificación falla, rechaza rápido y no ejecutes lógica de negocio. Devuelve un error cliente claro (comúnmente 400, 401 o 403 según las expectativas del proveedor). Registra solo lo que te ayude a diagnosticar: nombre del proveedor, ID de evento (si está), una razón breve como "firma inválida" o "timestamp demasiado antiguo", y tu propio request ID. Evita registrar bodies crudos o headers completos porque pueden incluir secretos.
Una arquitectura simple de webhook que se mantiene estable bajo carga
El patrón más fiable es aburrido: haz el mínimo trabajo en la petición HTTP y luego delega el trabajo real a un worker en background.
La ruta de petición segura y rápida
Cuando Stripe, GitHub o Slack llaman a tu endpoint, mantén la ruta corta y predecible:
- Verifica firma y headers básicos (rechaza rápido si son inválidos)
- Registra el evento y una clave de evento única
- Encola un job (o escribe una fila de "inbox")
- Devuelve una respuesta 2xx de inmediato
Devolver 2xx rápidamente importa porque los emisores de webhooks reintentan por timeouts y errores 5xx. Si haces trabajo lento (fan-out en BD, llamadas a APIs, envío de email) antes de responder, incrementas reintentos, entregas duplicadas y tráfico en avalancha durante incidentes.
Separar ingestión de procesamiento
Piénsalo como dos componentes:
- Endpoint de ingestión: comprobaciones de seguridad, validación mínima, encolar, 2xx
- Worker: lógica de negocio idempotente, reintentos y actualizaciones de estado
Esta separación mantiene tu endpoint estable bajo carga porque el worker puede escalar y reintentar sin bloquear nuevos eventos. Si Slack envía una ráfaga de eventos durante una importación de usuarios, el endpoint sigue siendo rápido mientras la cola absorbe el pico.
Para logging, captura lo necesario para depurar sin filtrar secretos o PII: tipo de evento, remitente (Stripe/GitHub/Slack), delivery ID, resultado de verificación de firma, estado de procesamiento y timestamps. Evita volcar headers completos o bodies en logs; almacena payloads solo en un event store protegido si realmente los necesitas.
Paso a paso: un patrón de manejador de webhooks que puedes copiar
La mayoría de errores de webhooks ocurren porque el manejador intenta hacerlo todo dentro de la petición HTTP. Trata la petición entrante como un paso de recibo y luego mueve el trabajo real a un worker.
El manejador de petición (rápido y estricto)
Este patrón funciona en cualquier stack:
- Valida la petición y captura el body crudo. Comprueba el método, la ruta esperada y el content-type. Guarda los bytes crudos antes de cualquier parseo JSON para que las comprobaciones de firma no se rompan.
- Verifica la firma temprano. Rechaza firmas inválidas con una respuesta 4xx clara. No "adivines" lo que quiso decir el payload.
- Extrae un ID de evento y construye una clave de idempotencia. Prefiere el ID de evento del proveedor. Si no existe, construye una clave con campos estables (origen + timestamp + acción + ID del objeto).
- Escribe un registro de idempotencia antes de efectos secundarios. Haz un insert atómico como "event_id no visto antes." Si ya existe, devuelve 200 y para.
- Encola trabajo y devuelve 200 rápidamente. Pon el evento (o un puntero al payload guardado) en una cola. La petición web no debería llamar APIs de terceros, enviar emails ni hacer trabajo pesado.
El worker (efectos secundarios seguros)
El worker carga el evento encolado, ejecuta la lógica de negocio y actualiza el registro de idempotencia a un estado claro como processing, succeeded o failed. Los reintentos pertenecen aquí, con backoff y un tope.
Ejemplo: llega dos veces un webhook de pago de Stripe. La segunda petición ve el mismo event ID, detecta el registro de idempotencia existente y sale sin volver a subir la cuenta del cliente.
Reintentos que ayudan en vez de empeorar
Los reintentos son útiles cuando el fallo es temporal. Son dañinos cuando convierten un bug real en un pico de tráfico, o cuando repiten una petición que nunca debería tener éxito.
Reintenta solo cuando haya buena probabilidad de que el siguiente intento funcione: timeouts de red, resets de conexión y respuestas 5xx de tus dependencias. No reintentes respuestas 4xx que significan "tu petición está mal" (firma inválida, JSON inválido, campos faltantes). Tampoco reintentes cuando ya sabes que el evento está duplicado y fue manejado con idempotencia.
Un conjunto de reglas simple:
- Reintentar: timeouts, límites 429, 500-599, errores DNS/conexión temporales
- No reintentar: 400-499 (excepto 429), firma inválida, validación de esquema fallida
- Tratar como éxito: evento ya procesado (replay idempotente)
- Parar rápido: la dependencia está caída para todos (usa un circuit breaker)
- Siempre: capear intentos y tiempo total
Usa backoff exponencial con jitter. En términos simples: espera un poco, luego más tiempo cada vez y añade un pequeño retraso aleatorio para que los reintentos no golpeen todos a la vez. Por ejemplo: 1s, 2s, 4s, 8s, más o menos hasta un 20% de aleatoriedad.
Fija tanto un número máximo de intentos como una ventana máxima de reintentos. Un punto de partida práctico es 5 intentos en 10 a 15 minutos. Esto evita bucles de reintento infinitos que esconden problemas hasta que explotan.
Haz que las llamadas descendentes sean seguras. Pon timeouts cortos en llamadas a BD y APIs, y añade un circuit breaker para dejar de llamar a un servicio fallido durante uno o dos minutos.
Finalmente, registra por qué reintentaste: timeout, 5xx, 429, nombre de la dependencia y cuánto tardó. Esas etiquetas convierten "a veces perdemos webhooks" en un problema reparable.
Manejo dead-letter: cómo dejar de perder eventos para siempre
Necesitas un plan para los eventos que no se procesan aunque se hayan reintentado. Una dead-letter queue (DLQ) es un lugar donde van las entregas de webhook que fallan repetidamente, para que no desaparezcan en logs ni queden atrapadas en reintentos eternos.
Un buen registro DLQ guarda suficiente contexto para depurar y reproducir sin adivinar:
- Payload crudo (como texto) y JSON parseado
- Headers que necesites para la verificación y trazabilidad (firma, ID de evento, timestamp)
- Mensaje de error y stack trace (o una razón corta de fallo)
- Conteo de intentos y timestamps de cada intento
- Tu estado interno de procesamiento (usuario creado, plan actualizado, etc.)
Luego haz que el replay sea seguro. Las reproducciones deben pasar por el mismo camino idempotente que el webhook en vivo, usando una clave de evento estable (normalmente el ID de evento del proveedor). Así, re-reproducir un evento dos veces no hace nada la segunda vez.
Un flujo sencillo ayuda a equipos no técnicos a actuar rápido sin tocar código. Por ejemplo, cuando un evento de pago falla por una caída temporal de la base de datos, alguien puede reproducirlo después de que el sistema esté sano.
Mantén el flujo mínimo:
- Auto-ruta fallos repetidos hacia la DLQ tras N intentos
- Muestra un mensaje corto de "qué falló" más un resumen del payload
- Permite reproducir (con idempotencia aplicada)
- Permite "marcar como ignorado" con una nota obligatoria
- Escala a ingeniería si el mismo error se repite
Fija retención y alertas. Conserva items de DLQ lo suficiente para cubrir fines de semana y vacaciones (a menudo 7 a 30 días) y alerta a un responsable claro cuando la DLQ crezca por encima de un umbral pequeño.
Ejemplo: prevenir upgrades duplicados desde un webhook de pago de Stripe
Un flujo común de Stripe: un cliente paga, Stripe envía un evento payment_intent.succeeded y tu app mejora la cuenta.
Así es como se rompe. Tu manejador recibe el evento, intenta actualizar la BD y llamar a una función de facturación. La base de datos se ralentiza, la petición agota el tiempo y tu endpoint devuelve un 500. Stripe asume que la entrega falló y reintenta. Ahora el mismo evento te golpea otra vez y el usuario recibe la mejora dos veces (o recibe dos créditos, dos facturas marcadas como pagadas o dos correos de bienvenida).
La solución es por capas:
Primero, verifica la firma de Stripe antes de hacer cualquier otra cosa. Si la firma es incorrecta, devuelve 400 y para.
Luego, haz el procesamiento idempotente usando el event.id de Stripe. Almacena un registro como processed_events(event_id) con una restricción única. Cuando llega el evento:
- Si
event_ides nuevo, acéptalo. - Si
event_idya existe, devuelve 200 y no hagas nada.
Después separa recepción y trabajo: validar + registrar + encolar, y deja que un worker haga la mejora. El endpoint responde rápido, así los timeouts son raros.
Finalmente, añade una ruta dead-letter. Si el worker falla por un error de base de datos, guarda el payload y la razón del fallo para reproducir con seguridad. Reproducir debe ejecutar el mismo código de worker y la idempotencia garantiza que no se double-upgrade.
Tras estos cambios, el usuario ve una sola mejora, hay menos demoras y muchas menos tickets de soporte de "pagué dos veces".
Errores comunes que crean bugs silenciosos en webhooks
La mayoría de bugs de webhooks no son ruidosos. Tu endpoint devuelve 200, los paneles parecen bien y semanas después notas upgrades faltantes, emails duplicados o registros desincronizados.
Un error clásico es romper la verificación de firma por accidente. Muchos proveedores firman el body crudo, pero algunos frameworks parsean JSON primero y cambian espacios o el orden de claves. Si verificas contra el body parseado, buenas peticiones pueden parecer manipuladas y ser rechazadas. La solución: verifica usando los bytes crudos exactamente como llegaron y luego parsea.
Otro fallo silencioso ocurre cuando devuelves 200 demasiado pronto. Si reconoces el webhook y luego falla tu procesamiento (escritura en BD, llamada a API de terceros, enqueue), el proveedor no reintentará porque ya dijiste que funcionó. Solo reconoce éxito después de que hayas registrado el evento de forma segura (o lo hayas encolado).
Hacer trabajo lento dentro del hilo de la petición también mata la fiabilidad. Los emisores de webhooks suelen tener timeouts cortos. Si haces lógica pesada o llamadas de red antes de responder, obtendrás reintentos, duplicados y eventos ocasionalmente perdidos.
Los bugs de dedupe pueden ser sutiles también. Si desduplicas por la clave equivocada como user ID o repo ID, descartarás eventos reales. La deduplicación debe basarse en el identificador único del evento (y a veces en el tipo de evento), no en a quién se refiere.
Finalmente, ten cuidado con los logs. Volcar payloads completos puede filtrar secretos, tokens, emails o IDs internos. Registra contexto mínimo (event ID, tipo, timestamps) y enmascara campos sensibles.
Lista rápida: ¿tu integración de webhooks es segura ahora?
Un manejador de webhooks es "seguro" cuando se mantiene correcto durante duplicados, reintentos, bases de datos lentas y peticiones malas ocasionales.
Comienza con lo básico que evita fraude y doble procesamiento:
- Verifica la firma usando el body crudo (antes de parsear JSON o cualquier transformación del body).
- Crea y almacena un registro de idempotencia antes de efectos secundarios. Guarda el ID de evento (o una clave calculada) primero y luego haz el trabajo.
- Devuelve un 2xx rápido tan pronto como la petición esté verificada y encolada de forma segura.
- Fija timeouts claros en todas partes. Handler, llamadas a BD, llamadas a APIs externas.
- Ten reintentos con backoff y un número máximo de intentos. Los reintentos deben desacelerarse con el tiempo y pararse tras un límite.
Luego comprueba que puedes recuperarte cuando algo aún falla:
- Existe almacenamiento dead-letter e incluye payload, headers necesarios, razón de error y conteo de intentos.
- El replay es real. Puedes re-ejecutar un evento dead-letter de forma segura tras arreglar el fallo, y la idempotencia evita efectos duplicados.
- Monitorización básica: contadores de recibidos, procesados, reintentados y dead-lettered, y una alerta cuando la DLQ crece.
Chequeo rápido: si tu servidor se reinicia a mitad de una petición, ¿perderías el evento o lo procesarías dos veces? Si no estás seguro, arregla eso primero.
Siguientes pasos si tus webhooks ya son frágiles
Si ya tienes eventos perdidos o duplicados raros, trata esto como un proyecto de reparación pequeño, no como un parche rápido. Elige una integración (Stripe, GitHub o Slack) y arréglala de punta a punta antes de tocar las demás.
Un orden práctico de operaciones:
- Añade verificación de firma primero y haz los fallos obvios en los logs.
- Haz el procesamiento idempotente (almacena un ID de evento y ignora repeticiones).
- Separa "recibir" de "procesar" (ack rápido, trabajo en background).
- Añade reintentos seguros con backoff para fallos temporales.
- Añade manejo dead-letter para que los eventos fallidos se guarden para revisión.
Luego escribe un pequeño plan de pruebas que puedas ejecutar cada vez que cambies código:
- Entrega duplicada: envía el mismo evento dos veces y confirma que solo se aplica una vez.
- Firma inválida: confirma que la petición se rechaza y no se procesa nada.
- Eventos fuera de orden: confirma que tu sistema se mantiene consistente.
- Downstream lento: simula un timeout para confirmar que los reintentos ocurren de forma segura.
Si heredaste código de webhooks generado por una IA y se siente frágil (difícil de seguir, efectos secundarios sorpresa, secretos en lugares raros), un pase de remediación enfocado suele ser más rápido que perseguir síntomas. FixMyMess (fixmymess.ai) ayuda a equipos a convertir prototipos generados por IA en código listo para producción auditando la lógica, endureciendo seguridad y reconstruyendo flujos de webhook inestables en un patrón de ingest-and-worker seguro.
Preguntas Frecuentes
Why do I get the same webhook event more than once?
Trata los duplicados como algo normal, no como una excepción. La mayoría de los proveedores entregan webhooks al menos una vez, así que un timeout o un breve 500 puede hacer que el mismo evento se envíe de nuevo incluso si tu código ya lo procesó una vez.
When should my webhook endpoint return 200?
Devuelve un 2xx solo después de que hayas verificado la firma y registrado el evento de forma segura (o lo hayas puesto en cola) de una manera que puedas recuperar. Si devuelves 200 y luego falla tu escritura en la base de datos o el enqueue, el proveedor asumirá que todo funcionó y no reintentará, lo que crea pérdida de datos silenciosa.
How do I prevent double-charging or double-upgrading from retries?
Usa idempotencia basada en una clave única estable, idealmente el ID de evento del proveedor. Almacena esa clave en un almacenamiento duradero con una restricción de unicidad, y si la vuelves a ver, sal temprano devolviendo éxito para que los reintentos se detengan.
How do I handle out-of-order webhook events without corrupting state?
No asumas orden; haz las actualizaciones condicionales usando versionado, marcas de tiempo o el estado actual para que un evento antiguo no sobrescriba datos más nuevos, y diseña los manejadores para que cada evento sea seguro de aplicar incluso si llega tarde.
Why does signature verification fail even when the secret is correct?
Verifica contra los bytes crudos del cuerpo de la petición exactamente como se recibieron, antes de cualquier parseo o re-serialización JSON. Muchos frameworks cambian espacios en blanco u orden de claves al parsear, y ese pequeño cambio basta para que una firma correcta parezca inválida.
Should my webhook handler do the business logic in the request thread?
Un buen patrón es verificar la firma, escribir un registro de inbox/idempotencia, poner el trabajo en cola y responder inmediatamente. Trabajo lento como envío de emails, llamadas a APIs de terceros o fan-out pesado en BD pertenece a un worker para que el proveedor no agote el tiempo y reintente.
Which failures should I retry, and which should I not retry?
Reintenta cuando la falla sea probablemente temporal, como timeouts, errores de red, límites 429 o respuestas 5xx de dependencias. No reintentes firmas inválidas o payloads mal formados, y siempre pon un tope de intentos y un límite total de tiempo para que las fallas sean visibles en vez de loop infinito.
What should I store for deduplication, and how long should I keep it?
Registra una clave de deduplicación, cuándo la viste por primera vez y un estado de procesamiento para distinguir recibido, completado o fallido. Para cualquier cosa que implique dinero o cambios de acceso, conserva el registro de dedupe de forma duradera y el tiempo suficiente para cubrir la ventana de reintentos del proveedor y tus propios reintentos diferidos.
What is a dead-letter queue and when do I need one?
Un dead-letter es donde van los eventos cuando los reintentos se agotan, para que no desaparezcan. Guarda suficiente contexto para entender el fallo y reproducir de forma segura, y asegúrate de que el replay pase por el mismo flujo idempotente para que una re-reproducción no cree efectos duplicados.
My webhooks are brittle and were generated by an AI tool—what’s the fastest way to fix them?
Normalmente te falta una de las capas básicas de seguridad: verificación de firma, idempotencia, reconocimiento rápido con procesamiento en background, reintentos controlados o visibilidad en dead-letter. Si el código fue generado por una IA y es difícil de razonar, FixMyMess (fixmymess.ai) puede auditar el flujo, corregir lógica y seguridad, y reconstruirlo en un patrón ingest-and-worker de forma rápida.