La idempotencia en sistemas distribuidos que realmente funciona
Evita efectos secundarios duplicados
La idempotencia en sistemas distribuidos es la propiedad que te salva cuando la red miente, las colas reintentan, el cliente entra en pánico y el operador pulsa “repetir”. En sistemas de producción, la entrega duplicada es normal. Los efectos secundarios duplicados son el error.
HTTP define un método idempotente como aquel en el que múltiples solicitudes idénticas tienen el mismo efecto intencional en el servidor que una sola solicitud. Es por eso que PUT, DELETE y los métodos seguros son idempotentes en la semántica del protocolo y pueden reintentarse automáticamente tras una falla de comunicación.

Esa definición es útil, pero no es suficiente. En arquitecturas reales, la idempotencia no es una respuesta trivial de HTTP. Es una garantía de negocio. Si un cliente pulsa “pagar” una vez, no puedes cobrarle dos veces porque hubo un tiempo de espera entre el commit y la respuesta. Si un trabajador actualiza el inventario y se bloquea antes de confirmar (ack) el mensaje, no puedes decrementar el stock dos veces solo porque el broker volvió a entregar el mismo mensaje. Esa es la norma.
El error que veo una y otra vez es tratar la idempotencia como una característica del transporte en lugar de una propiedad del sistema. La deduplicación de colas, los verbos HTTP y los reintentos del cliente ayudan, pero ninguno rescata un diseño que permite que la misma intención de negocio genere un segundo efecto secundario. Si deseas el marco conceptual más amplio sobre cómo estas decisiones de integración se ajustan a los límites del servicio y los compromisos de persistencia, comienza con Arquitectura de Aplicaciones en Producción: Patrones de Integración, Diseño de Código y Acceso a Datos.
De dónde provienen los duplicados en producción
Los duplicados no aparecen porque los equipos sean descuidados. Aparecen porque los sistemas distribuidos reintentan, reordenan y repiten.
Un cliente puede enviar una solicitud de creación, el servidor puede confirmarlos, y la respuesta aún puede desaparecer en el cable. Es exactamente por eso que HTTP distingue los métodos idempotentes y por qué las APIs de pago como Stripe y PayPal exponen mecanismos explícitos de idempotencia para métodos inseguros como POST.
Los brokers de mensajes hacen el problema aún más obvio. La entrega “al menos una vez” significa que un consumidor puede ser invocado repetidamente para el mismo mensaje, y un gestor puede actualizar la base de datos con éxito pero fallar antes del reconocimiento, lo que provoca que el broker vuelva a entregar el mismo mensaje.
Los webhooks no son diferentes. GitHub dice que las entregas de webhooks pueden llegar fuera de orden, las entregas fallidas no se reintentan automáticamente y cada entrega lleva un GUID único X-GitHub-Delivery que debes usar al protegerte contra repeticiones. Para una visión práctica de la arquitectura de los puntos finales de chat como límites de interacción, consulta Plataformas de Chat como Interfaces de Sistema en Sistemas Modernos.
Incluso los sistemas que anuncian garantías más fuertes aún te dejan trabajo por hacer. Kafka puede prevenir entradas duplicadas en los logs de Kafka con productores idempotentes y puede proporcionar entrega exactamente una vez para flujos de lectura-procesamiento-escritura que permanecen dentro de Kafka con transacciones y consumidores read_committed. Pero los propios documentos de diseño de Kafka son claros: los sistemas externos aún requieren coordinación con desplazamientos (offsets) y salidas. La entrega exactamente una vez de Google Cloud Pub/Sub está limitada a suscripciones de extracción (pull), dentro de una región de la nube, y aún requiere que los clientes rastreen el progreso del procesamiento hasta que el reconocimiento tenga éxito.
Mi resumen opinativo es simple. Asume que el transporte reintentará. Asume que los operadores repetirán. Asume que los webhooks llegarán tarde. Diseña la ruta de escritura para que una intención repetida no pueda crear un segundo efecto de negocio. El diseño de errores está estrechamente relacionado: cómo se envuelven, traducen y clasifican los errores como reintentables versus no reintentables es parte de la misma disciplina de límites — Arquitectura de Manejo de Errores en Go: Límites y Patrones cubre la clasificación de errores reintentables, la traducción de límites y los patrones centinela que permiten que la lógica de reintento tome decisiones sólidas. Cuando los reintentos siguen golpeando una dependencia no saludable, un breaker de circuito en el límite de integración falla rápido antes de que las tormentas de reintento amplifiquen el trabajo duplicado.
El contrato de API en el que realmente confío
Cómo las claves de idempotencia previenen solicitudes duplicadas de API
El único contrato de API en el que confío para operaciones de mutación es la intención proporcionada por el llamador más la persistencia del lado del servidor.
AWS recomienda un identificador de solicitud proporcionado por el llamador y advierte que el servicio debe registrar atómicamente el token de idempotencia junto con el trabajo de mutación. Stripe almacena el primer código de estado y el cuerpo de la respuesta para una clave, compara los parámetros posteriores con la solicitud original y devuelve el mismo resultado para los reintentos. PayPal usa PayPal-Request-Id en las APIs POST compatibles y devuelve el estado más reciente de la solicitud anterior con ese mismo encabezado.
Esto lleva a un contrato práctico:
- El cliente genera una clave de idempotencia para una operación de negocio.
- El servidor acota esa clave por inquilino (tenant) y nombre de operación.
- El servidor almacena un hash de solicitud para que la misma clave no pueda reutilizarse para una carga útil diferente.
- El servidor registra estados como
pending,completedofailed. - Los reintentos con la misma clave devuelven el resultado almacenado o un puntero estable al mismo.
- Los reintentos con la misma clave y una carga útil diferente fallan ruidosamente.
Hay un borrador de IETF Idempotency-Key, pero a partir del 09/05/2026 todavía está listado en el IETF Datatracker como un Internet-Draft caducado en lugar de un RFC publicado. En la práctica, el nombre del encabezado sigue siendo muy útil como una convención de facto, pero debes documentar el contrato en tu propia API en lugar de fingir que el estándar está terminado.
¿Qué debe representar la clave? La intención. No un intento HTTP. No una conexión TCP. No un contador de reintentos. Si el usuario quiere “crear la orden 123 una vez”, cada reintento para ese mismo comando debe reutilizar la misma clave. Si el usuario quiere “realizar una segunda orden”, eso debe usar una clave diferente.
Un ID de solicitud es para rastreo. Una clave de idempotencia es para corrección. Si confundes esos conceptos, tus paneles de control se ven ordenados mientras tu dinero se mueve dos veces.
Por qué PUT no es suficiente
No, HTTP PUT no es suficiente para hacer que una operación sea idempotente.
Sí, RFC 9110 otorga a PUT semántica idempotente. Pero si tu manejador PUT emite un nuevo evento downstream, envía un correo electrónico en cada reintento o cobra nuevamente a un proveedor externo, entonces tu implementación ha violado el contrato de negocio aunque el nombre de tu ruta parezca respetable.
La elección del verbo ayuda a los clientes a entender la intención. No implementa la intención por ti.
Usa PUT cuando el modelo de recursos se ajuste genuinamente a una operación de reemplazo completo o upsert. Usa POST cuando estás creando comandos o acciones. Pero para cualquier mutación que pueda reintentarse a través de límites de red, documenta un contrato de idempotencia explícito. Si tus acciones de mutación son activadas desde flujos de trabajo de chat, el mismo contrato se aplica en Patrones de Integración de Slack para Alertas y Flujos de Trabajo y Patrón de Integración de Discord para Alertas y Bucles de Control. Los efectos secundarios ocultos son donde la arquitectura va a morir.
¿Cuánto tiempo debe almacenarse una clave de idempotencia
Más tiempo del que quiere tu equipo de transporte.
Stripe dice que las claves pueden eliminarse después de al menos 24 horas. PayPal dice que la retención es específica de la API y da ejemplos que pueden durar hasta 45 días. Amazon SQS FIFO deduplica solo dentro de una ventana de 5 minutos. GitHub mantiene las entregas recientes durante 3 días para reintento manual. Esos números son ampliamente diferentes porque el período de retención adecuado es una decisión de negocio, no un protocolo por defecto.
Si solo mantienes las claves durante cinco minutos porque tu cola lo hace, no estás diseñando idempotencia. Estás copiando una limitación del transporte en tu capa de negocio.
Mantén los registros de idempotencia durante al menos el máximo de estas ventanas:
- horizonte de reintento del cliente
- horizonte de reestructuración de la cola
- horizonte de repetición de webhooks
- horizonte de repetición del operador
- horizonte de liquidación o compensación para operaciones que mueven dinero
Para pagos, reservas y aprovisionamiento, eso a menudo significa horas o días, no minutos.
AWS también señala dos antipatrones con los que estoy totalmente de acuerdo. No uses marcas de tiempo como clave, porque el desajuste del reloj y las colisiones las hacen poco confiables. No almacenes ciegamente cargas útiles de solicitud enteras como registro de deduplicación para cada solicitud, porque eso perjudica el rendimiento y la escalabilidad. Almacena un hash de solicitud normalizado más el estado de respuesta mínimo que necesitas para repetir con seguridad. Si debes reproducir el primer byte de respuesta byte por byte, almacena el cuerpo de respuesta canónico como lo hace Stripe.
Los patrones de base de datos que hacen real la idempotencia
La idempotencia se vuelve real cuando la capa de persistencia puede ganar una carrera exactamente una vez.
PostgreSQL te da dos primitivas críticas aquí. Las restricciones únicas garantizan la unicidad en una o más columnas, y INSERT ... ON CONFLICT te permite definir una acción alternativa en lugar de fallar por una violación de unicidad. PostgreSQL también documenta que ON CONFLICT DO UPDATE garantiza un resultado atómico de inserción-actualización bajo concurrencia.
Eso significa que tu capa de idempotencia debería comenzar usualmente con una tabla como esta:
create table api_idempotency (
tenant_id text not null,
operation text not null,
idempotency_key text not null,
request_hash text not null,
state text not null,
status_code integer,
response_body jsonb,
resource_type text,
resource_id text,
created_at timestamptz not null default now(),
expires_at timestamptz not null,
primary key (tenant_id, operation, idempotency_key)
);
Y el flujo de manejo debería verse así:
begin transaction
try insert (tenant_id, operation, idempotency_key, request_hash, state='pending')
on conflict do nothing
load row for (tenant_id, operation, idempotency_key) for update
if row.request_hash != incoming_request_hash
fail with conflict or validation error
if row.state = 'completed'
return stored response
if row.state = 'pending' and row was created by another live request
either wait briefly, or fail fast with a retryable response
perform local business mutation
store stable result in idempotency row
set state = 'completed'
commit
return result
La parte importante no es la sintaxis. La parte importante es la atomicidad. Registrar la clave y realizar la mutación deben tener éxito o fallar juntos. AWS dice esto explícitamente para la idempotencia de API, y la misma regla se aplica en servicios respaldados por SQL.
No hagas una secuencia ingenua de verificar-actuar como “seleccionar clave; si falta entonces insertar orden”. Bajo concurrencia, dos solicitudes pueden pasar la verificación y ambas crear el efecto secundario. Una restricción única no es opcional. Es el mecanismo que convierte tu arquitectura de folklore optimista en algo que puedes demostrar bajo carga.
Aquí está la regla que uso en revisiones. Si la decisión de deduplicación no está protegida por el mismo límite transaccional que la mutación, no tienes idempotencia. Tienes esperanza.
Los mensajes, eventos y webhooks necesitan su propio límite
Cómo los consumidores manejan eventos y mensajes duplicados
Para consumidores de mensajes, el patrón clásico sigue siendo el correcto. Registra los ID de mensajes procesados en la misma transacción de base de datos que la actualización de negocio. Chris Richardson describe el enfoque de la tabla PROCESSED_MESSAGES directamente, usando una clave primaria por suscriptor e ID de mensaje para que los duplicados fallen limpiamente y puedan ser ignorados.
Muchos equipos llaman a esa tienda explícita processed_messages una tabla de bandeja de entrada (inbox). La etiqueta importa menos que la regla. El receptor debe persistir la prueba de que ya manejó el mensaje antes de que un reintento pueda hacer nada con seguridad.
Una forma mínima se ve así:
create table processed_messages (
subscriber_id text not null,
message_id text not null,
processed_at timestamptz not null default now(),
primary key (subscriber_id, message_id)
);
Y el flujo del consumidor es tan estricto como el flujo HTTP:
begin transaction
insert into processed_messages (subscriber_id, message_id)
values (?, ?)
on conflict do nothing
if no row inserted
rollback
ack and ignore duplicate
apply business mutation
commit
ack message
Ese patrón es aburrido. Bien. La idempotencia debería ser aburrida.
También suele ser mejor que intentar apoyarse en términos de marketing de brokers. El soporte de exactamente una vez de Kafka es excelente cuando te mantienes dentro del modelo transaccional propio de Kafka, pero los documentos de Kafka aún advierten que los destinos externos necesitan cooperación. SQS FIFO reduce los envíos duplicados solo dentro de su ventana de deduplicación de 5 minutos. Pub/Sub exactamente una vez aún espera que el suscriptor rastree el progreso y evite el trabajo duplicado cuando los reconocimientos fallen.
Exactamente una vez suele ser una optimización local. Los efectos secundarios idempotentes son la garantía del sistema.
Combina la deduplicación con el patrón outbox
Si tu servicio actualiza el estado local y también publica un evento, la consumo idempotente por sí solo no es suficiente. También necesitas una forma segura de sacar el evento después de que la transacción local se confirme.
Es por eso que el patrón de caja de salida transaccional importa. Chris Richardson describe la idea básica como escribir el evento en una tabla outbox en la misma transacción que la actualización de negocio, y luego publicarlo asincrónicamente. Debezium dice que el patrón outbox evita inconsistencias entre el estado interno de un servicio y los eventos consumidos por otros servicios. NServiceBus va más allá y muestra cómo el procesamiento outbox deduplica mensajes entrantes y evita registros zombis y mensajes fantasma.
Esta es la arquitectura que recomiendo para servicios que poseen datos y publican eventos de integración:
- Valida y persiste el comando bajo una clave de idempotencia.
- Escribe el estado de negocio y el evento outbox en una transacción local única.
- Deja que CDC o un despachador outbox publiquen el evento.
- Haz que los consumidores downstream también sean idempotentes.
Outbox no elimina la necesidad de consumidores idempotentes. Elimina la necesidad de fingir que un commit de base de datos y una publicación de broker pueden ser una transacción distribuida mágica única cuando usualmente no pueden.
Los webhooks son solo mensajes con mejor marca
Trata los webhooks entrantes exactamente como mensajes desde un borde de red no confiable.
GitHub documenta que las entregas pueden llegar fuera de orden, recomienda usar X-Hub-Signature-256 para verificar la autenticidad y proporciona X-GitHub-Delivery como el identificador de entrega único. También nota que los reintentos reutilizan el mismo ID de entrega.
Así que la arquitectura es sencilla:
- verifica la firma primero
- usa el GUID de entrega como clave de deduplicación
- persiste el recibo antes de los efectos secundarios
- haz que los manejadores sean conscientes del orden en lugar de asumir el orden de llegada
- encola el trabajo pesado y retorna rápido
Si tu manejador de webhook escribe directamente en las tablas de negocio antes de registrar el recibo, no está listo para producción. Solo es más rápido cometiendo errores duplicados.
Las sagas y los motores de flujo de trabajo aún necesitan idempotencia
Las sagas y los motores de flujo de trabajo duraderos no eliminan el problema. Lo hacen visible.
Temporal recomienda escribir Actividades para que sean idempotentes porque las Actividades pueden reintentarse después de fallas o tiempos de espera. Sus documentos incluso señalan el caso límite donde un trabajador completa un efecto secundario externo con éxito pero se bloquea antes de informar la finalización, lo que provoca que la Actividad se ejecute nuevamente. Temporal también sugiere usar una combinación de Workflow Run ID y Activity ID como una clave de idempotencia estable al llamar a servicios downstream. Si estás aplicando esto en orquestación de servicios, Microservicios Go para Orquestación de IA/ML cubre los compromisos más amplios del flujo de trabajo.
Ese es exactamente el modelo mental correcto. Un motor de flujo de trabajo puede preservar el historial de ejecución y coordinar reintentos. No puede deshacer un cargo en una tarjeta o reenviar un correo electrónico retroactivamente a menos que tu aplicación le proporcione pasos idempotentes y compensaciones idempotentes.
Lo mismo se aplica a las sagas. La propia guía de sagas de Temporal describe acciones compensatorias que se ejecutan cuando falla un paso. Esas compensaciones también deben ser idempotentes. Si “reembolsar pago” se ejecuta dos veces, puedes haber resuelto el error original creando uno nuevo.
Mi regla aquí es brutal y simple. Cada Actividad, cada manejador de comandos y cada compensación que toque el mundo exterior debería ser naturalmente idempotente o llevar una clave de idempotencia real al sistema downstream.
Cómo probar la idempotencia antes de producción
La mayoría de los equipos prueban caminos felices y luego actúan sorprendidos cuando ocurren reintentos. Eso no es suficiente. Para equipos de Go, Pruebas de Código Concurrente en Go con testing/synctest cubre cómo escribir pruebas rápidas y deterministas para bucles de reintento y comportamiento de límite de tiempo de contexto sin dormir a través de retrasos artificiales.
Deberías tener pruebas automatizadas para al menos estos casos:
- el servidor confirma la mutación pero la respuesta nunca llega al cliente
- dos solicitudes idénticas compiten con la misma clave de idempotencia
- la misma clave se reutiliza con una carga útil diferente
- un consumidor confirma su trabajo de base de datos y se bloquea antes del ack
- un webhook se repite con el mismo ID de entrega
- un despachador outbox publica el mismo evento más de una vez
- una Actividad de flujo de trabajo completa la llamada externa y se bloquea antes de que se informe la finalización
- un registro de idempotencia expira y llega un reintento genuino tardío
AWS recomienda explícitamente suites de pruebas integrales que incluyan solicitudes exitosas, solicitudes fallidas y solicitudes duplicadas. Ese consejo es pedestre y absolutamente correcto.
Yo añadiría un ejercicio de fallo más. Verifica que la respuesta repetida sea semánticamente equivalente al primer resultado. AWS discute los reintentos de llegada tardía y argumenta a favor de respuestas que preservan el significado original incluso después de que el estado subyacente haya cambiado. Esa es la diferencia entre “no ocurrió un efecto secundario extra” y “el llamador aún tiene un contrato consistente.”
Reglas opinativas que salvan sistemas reales
Aquí están las reglas que haría cumplir en una revisión de arquitectura.
Primero, las claves de idempotencia pertenecen a la intención de negocio, no a los intentos de transporte.
Segundo, acota cada clave por inquilino (tenant) y operación. Los espacios de clave globales son cómo las solicitudes no relacionadas colisionan.
Tercero, persiste la decisión de deduplicación atómicamente con la mutación. Si eso no es cierto, el diseño está mal.
Cuarto, rechaza los reintentos de misma clave y diferente carga útil. Stripe y AWS hacen esto por una buena razón.
Quinto, mantén las claves durante todo el horizonte de repetición del proceso de negocio, no por la ventana de cola más corta.
Sexto, combina productores con una caja de salida (outbox) y consumidores con rastreo de ID de mensaje. Un lado sin el otro es medio diseño.
Séptimo, propaga la misma identidad de operación downstream cuando la acción de negocio es la misma. AWS recomienda explícitamente pasar el token de idempotencia a lo largo de la cadena de procesamiento.
Octavo, nunca asumas que el marketing de “exactamente una vez” elimina la necesidad de efectos secundarios idempotentes.
Si eso suena estricto, bien. La idempotencia es donde la arquitectura optimista se encuentra con la realidad de producción. No necesitas complejidad en todas partes. Pero dondequiera que los efectos secundarios duplicados puedan dañar dinero, estado o confianza, la idempotencia debería ser una parte de primera clase del contrato.
Estas mismas reglas se aplican directamente a agentes de fondo de IA. Los agentes de sondeo que reclaman tareas, emiten notificaciones o activan llamadas de herramientas necesitan claves de deduplicación y protocolos de reclamación idempotentes tanto como las APIs de pago. Para saber cómo funciona el patrón de reclamación y deduplicación dentro de asistentes de IA en producción, consulta Agentes de Sondeo en Asistentes de IA: 11 Patrones de Implementación.