Entrega at-most-once vs. at-least-once vs. exactly-once: ¿qué garantiza cada una en realidad, y es posible el exactly-once de verdad?
Cada semántica cambia un tipo de fallo por otro —pérdida, duplicación o ninguno— y resulta que 'exactly-once' es en realidad at-least-once disfrazado de idempotencia.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Entrega at-most-once vs. at-least-once vs. exactly-once: ¿qué garantiza cada una en realidad, y es posible el exactly-once de verdad?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#"Exactamente una vez" no es lo que parece
La semántica de entrega describe qué promete hacer un sistema de mensajería justo en el momento en que algo sale mal: un broker se reinicia, un corte de red tira una conexión, un consumidor se cae a mitad del procesamiento. Existen exactamente tres contratos posibles, y entender el tipo de fallo que produce cada uno es más útil que memorizar las definiciones.
#Como mucho una vez (at-most-once): los mensajes se pueden perder, nunca duplicar
El sistema entrega un mensaje cero o una vez. Si algo falla después de que el broker lo envía pero antes de que el consumidor termine, el mensaje simplemente desaparece — nadie lo reintenta. Esto es lo que obtienes por defecto con protocolos "dispara y olvida" tipo UDP, o con un consumidor que confirma (o hace commit de su offset) antes de procesar el mensaje.
At-most-once es aceptable para datos donde perder un mensaje ocasional sale más barato que la complejidad de garantizar la entrega — un flujo de telemetría periódica de sensores donde la siguiente lectura llega en un segundo de todas formas, o un indicador de "el usuario está escribiendo". Casi nunca es aceptable para nada con consecuencias de negocio: pagos, descuentos de inventario, confirmaciones de pedidos.
#Al menos una vez (at-least-once): los mensajes se pueden duplicar, nunca perder (en silencio)
Invierte el orden: el consumidor confirma (o hace commit) después de terminar de procesar. Ahora, si el consumidor se cae a mitad del procesamiento, el broker nunca recibió el ack, así que reentrega el mismo mensaje — posiblemente al mismo consumidor cuando se reinicie, posiblemente a otro. El mensaje nunca se pierde en silencio, pero puede procesarse dos veces.
At-least-once es la postura por defecto de la mayoría de los sistemas en producción (Kafka con commits de offset manuales, colas estándar de SQS, RabbitMQ con acks manuales) porque perder datos casi siempre es peor que procesarlos dos veces — siempre que puedas neutralizar el duplicado. Y ahí llegamos a la respuesta real a "¿de verdad es posible el exactly-once?"
#Exactamente una vez: la garantía que en realidad quieres, lograda sin necesitarla
El verdadero exactly-once de punta a punta —el mensaje cruza la red exactamente una vez y produce su efecto exactamente una vez, garantizado solo por la capa de mensajería— choca con el mismo muro que el clásico "Problema de los Dos Generales": no puedes lograr que dos partes independientes (el broker y el efecto externo del consumidor, como una escritura en base de datos o una llamada a una API de terceros) lleguen a un acuerdo perfecto sobre una red no confiable, porque la última confirmación de cualquier protocolo siempre se puede perder.
Lo que la industria llama en realidad "exactly-once" es una de estas dos cosas, y ambas dependen silenciosamente de una entrega at-least-once por debajo:
- Procesamiento idempotente sobre una entrega at-least-once. El mensaje puede llegar dos veces, pero el efecto ocurre una sola vez, porque el consumidor reconoce el duplicado y no hace nada en el segundo intento. Esta es la respuesta práctica y portable, y funciona con cualquier broker.
- Una garantía más estrecha, a nivel de protocolo, dentro de un solo sistema. La "semántica exactly-once" de Kafka (productor idempotente + transacciones) sí evita escrituras duplicadas dentro de Kafka — el reintento de un productor no crea dos copias de un registro en una partición, y un pipeline de consumir-transformar-producir hace commit de los offsets de entrada y los registros de salida de forma atómica. Eso es real y valioso, pero se detiene en la frontera de Kafka: en el momento en que el efecto de tu consumidor es una llamada HTTP externa, una fila en Postgres, o el envío de un correo, vuelves a necesitar idempotencia también en esa frontera.
Las formas habituales de hacer idempotente el procesamiento:
- Idempotencia natural:
SET balance = 100es idempotente sin importar cuántas veces se ejecute;balance = balance + 10no lo es. Prefiere la primera forma cuando puedas. - Claves de idempotencia / tablas de deduplicación: guarda el ID único del mensaje en la misma transacción que su efecto secundario (p. ej. "inserta en
processed_messages(msg_id)y luego actualiza el pedido — ambas cosas en una sola transacción de base de datos"). Un reintento ve el ID ya presente y se salta el trabajo. - Upserts en lugar de inserts:
INSERT ... ON CONFLICT (order_id) DO NOTHINGconvierte un insert duplicado en un no-op. - El patrón de outbox transaccional: escribe el cambio de negocio y la fila "evento a publicar" en la misma transacción local, y luego un relay aparte publica desde la tabla outbox — esto evita el clásico problema de la doble escritura (la BD hizo commit, pero la publicación falló, o viceversa) que es lo que crea duplicados o pérdidas en primer lugar.
| Semántica | Garantía | Modo de fallo | Mecanismo típico |
|---|---|---|---|
| At-most-once | Se entrega 0 o 1 veces | Pérdida silenciosa del mensaje | Ack/commit antes de procesar |
| At-least-once | Se entrega ≥ 1 veces | Procesamiento duplicado | Ack/commit después de procesar, reentrega si no hay ack |
| "Exactly-once" | El efecto ocurre exactamente una vez | Ninguno, si la idempotencia está bien implementada | Entrega at-least-once + consumidor idempotente (clave de dedup, upsert, outbox) |