Módulo 1 Del webhook al trabajo que sobrevive a los fallos
El incidente: tres entregas, tres cambios en el CRM
7 min
El CRM envía tres veces el evento evt-742. Las tres copias son idénticas y representan una sola cosa: un cliente ha escrito que quiere darse de baja.
La primera versión de NexoCare, el baseline (la versión de partida, sin protecciones), hace esto con cada entrega:
parsear el JSON → clasificar con el modelo → actualizar el CRM → responder 200
Tres entregas, tres actualizaciones en el CRM. Después llega el evento con secuencia 19 antes que el 18, y el baseline los procesa en ese orden, así que el estado final de la cuenta depende de cuál llegó el último. El script de esta lección solo reproduce los duplicados. Cómo se resuelve el desorden lo explica la lección 7, y lo verás pasar en el escenario C de live:crash, en la lección 10.
Reprodúcelo
En la carpeta raíz del proyecto:
npm run baseline:naive
Qué deberías ver:
{
status: 'EXPECTED_UNSAFE',
deliveries: 3,
uniqueIntentions: 1,
writes: 3,
receipts: [ 'crm_cus-742_2', 'crm_cus-742_3', 'crm_cus-742_4' ]
}
Qué significa: tres entregas, una sola intención y tres escrituras, cada una con su recibo y su versión nueva de la cuenta. EXPECTED_UNSAFE indica que el fallo es intencionado: el script reproduce el incidente para darte un punto de partida. Si algún día no lo reprodujera, terminaría con error.
Abre scripts/run-naive-baseline.ts. La función naiveHandle es el baseline entero: parsea el JSON sin mirar la firma, clasifica con el mismo clasificador de reglas que usa el resto del proyecto y escribe en el CRM simulado con randomUUID() como clave. Una clave nueva en cada entrega le dice al CRM que cada copia es otra intención, y el CRM obedece.
Los reintentos son lo normal
Es tentador pensar que el CRM se ha equivocado. Pero repetir entregas es el comportamiento documentado de casi cualquier emisor de webhooks. Stripe, por ejemplo, documenta que:
- en modo real reintenta durante un máximo de tres días, con esperas que crecen de forma exponencial, si no le respondes con éxito;
- reenviar un evento a mano no cancela sus reintentos automáticos, así que los dos pueden coexistir;
- no garantiza que los eventos lleguen en el orden en que se generaron;
- a veces el mismo evento llega más de una vez, y recomienda registrar los ID de evento ya procesados.
Hay más causas: el emisor no recibió tu respuesta porque la red cortó, un operador reenvió a mano o dos colas distintas procesaron la misma copia. Cualquier frontera con reintentos repetirá mensajes tarde o temprano. Diseñar contando con que «seguramente solo llegará una vez» es diseñar el incidente.
Por qué «exactly once» no se compra
«Exactly once» (exactamente una vez) es la promesa de que cada mensaje se procesa una sola vez. Ninguna cola te la regala en una integración con sistemas externos, porque la cola no controla la red, ni tu worker, ni el CRM.
Lo que sí puedes construir es un efecto único: que una intención produzca como mucho un cambio de negocio, aunque llegue repetida y aunque cada pieza falle por su lado. Para eso tienes que responder cuatro preguntas:
- ¿Qué identifica la intención recibida? Una clave estable.
- ¿Dónde se guarda antes de responder al emisor? Un registro persistente.
- ¿Qué worker puede reclamarla para procesarla? Exclusión entre workers.
- ¿Qué sistema demuestra el efecto final? Un recibo del CRM.
El baseline no responde a ninguna. No tiene inbox (un registro de lo recibido) ni recibo del efecto. Por eso tres entregas se convierten en tres escrituras: ninguna pieza conserva la identidad de lo que llegó.
El orden es otro problema
El caso de la secuencia 18 y 19 se parece al de los duplicados, pero necesita otra solución. Deduplicar te protege de la misma copia repetida. El orden necesita otro dato, un número de secuencia por entidad (aquí, por cliente), y una comprobación justo antes de actuar.
Lo verás en el inbox del proyecto: si ya se procesó el 19, el 18 queda registrado como obsoleto y no llega a tocar el CRM. El evento no se pierde (queda para auditoría), simplemente no se ejecuta.
Antes de seguir, responde a las cuatro preguntas con tus palabras. La siguiente lección asigna cada respuesta a una pieza concreta del sistema.