Idempotencia en sistemas distribuidos que realmente funciona
Evitar efectos secundarios duplicados
La idempotencia en sistemas distribuidos es la propiedad que te salva cuando la red falla, la cola reintentos, el cliente entra en pánico y el operador realiza una reproducción. En los sistemas de producción, la entrega duplicada es normal. Los efectos secundarios duplicados son el error.
HTTP define un método idempotente como aquel donde múltiples solicitudes idénticas tienen el mismo efecto previsto en el servidor que una sola solicitud. Por eso PUT, DELETE y los métodos seguros son idempotentes en la semántica del protocolo y pueden reintentarse automáticamente después de una falla de comunicación.

Esa definición es útil, pero no es suficiente. En las arquitecturas reales, la idempotencia no es una respuesta de trivia sobre HTTP. Es una garantía de negocio. Si un cliente presiona “pagar” una vez, no tienes derecho a cobrar dos veces porque ocurrió un tiempo de espera entre el compromiso y la respuesta. Si un trabajador actualiza el inventario y falla antes de confirmar el mensaje, no tienes derecho a decrementar el stock dos veces porque el broker reenvió el mensaje. Ese es el estándar.
El error que veo una y otra vez es tratar la idempotencia como una función de transporte en lugar de una propiedad del sistema. La deduplicación de colas, los verbos HTTP y los reintentos del cliente ayudan, pero ninguno de ellos rescata un diseño que permita que la misma intención de negocio cree un segundo efecto secundario. Si deseas una visión más amplia de cómo estas decisiones de integración se ajustan a los límites de los servicios y a 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 vienen los duplicados en producción
Los duplicados no aparecen porque los equipos sean descuidados. Aparecen porque los sistemas distribuidos reintentan, reordenan y reproducen.
Un cliente puede enviar una solicitud de creación, el servidor puede confirmarla, y la respuesta aún puede desaparecer en la red. Por eso HTTP distingue 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 evidente. La entrega al menos una vez significa que un consumidor puede ser invocado repetidamente para el mismo mensaje, y un manejador puede actualizar la base de datos con éxito pero fallar antes de la confirmación, causando que el broker entregue el mismo mensaje nuevamente.
Los webhooks no son diferentes. GitHub dice que las entregas de webhooks pueden llegar fuera de orden, las entregas fallidas no se reenvían automáticamente, y cada entrega lleva un GUID único X-GitHub-Delivery que deberías usar al protegerte contra la reproducción. Para una visión arquitectónica práctica 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 registros de Kafka con productores idempotentes y puede proporcionar entrega exactamente una vez para flujos de lectura-procesamiento-escritura que permanezcan dentro de Kafka con transacciones y consumidores read_committed. Pero los propios documentos de diseño de Kafka son claros de que los sistemas externos aún requieren coordinación con desplazamientos y salidas. La entrega exactamente una vez de Google Cloud Pub/Sub está limitada a suscripciones de extracción, dentro de una región de la nube, y aún requiere que los clientes rastreen el progreso del procesamiento hasta que la confirmación tenga éxito.
Mi resumen de opinión es simple. Asume que el transporte reintentará. Asume que los operadores reproducirá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 contrato de API en el que realmente confío
¿Cómo evitan las claves de idempotencia las solicitudes de API duplicadas
El único contrato de API en el que confío para operaciones de mutación es la intención proporcionada por el llamante más la persistencia del lado del servidor.
AWS recomienda un identificador de solicitud proporcionado por el llamante y advierte que el servicio debe registrar de forma atómica el token de idempotencia junto con el trabajo de mutación. Stripe almacena el primer código de estado y cuerpo de 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 admitidas y devuelve el estado más reciente para la solicitud anterior con esa misma cabecera.
Eso conduce a un contrato práctico:
- El cliente genera una clave de idempotencia para una operación de negocio.
- El servidor delimita esa clave por inquilino 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 el estado como
pending(pendiente),completed(completado) ofailed(fallido). - Los reintentos con la misma clave devuelven el resultado almacenado o un puntero estable a él.
- Los reintentos con la misma clave y una carga útil diferente fallan de manera evidente.
Existe un borrador de cabecera Idempotency-Key de IETF, pero a fecha de 2026-05-09 aún se lista en el Datatracker de IETF como un Borrador de Internet expirado en lugar de un RFC publicado. En la práctica, el nombre de la cabecera sigue siendo ampliamente útil como una convención de facto, pero deberías documentar el contrato en tu propia API en lugar de fingir que el estándar está terminado.
¿Qué debería representar la clave? La intención. No un intento HTTP. No una conexión TCP. No un contador de reintento. Si el usuario significa “crear el pedido 123 una vez”, cada reintento para ese mismo comando debe reutilizar la misma clave. Si el usuario significa “realizar un segundo pedido”, eso debe usar una clave diferente.
Un ID de solicitud es para trazabilidad. Una clave de idempotencia es para corrección. Si mezclas esas cosas, tus paneles de control se verán 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 semánticas idempotentes a PUT. Pero si tu manejador de PUT emite un nuevo evento aguas abajo, envía un correo electrónico en cada reintento o cobra a un proveedor externo nuevamente, entonces tu implementación ha violado el contrato de negocio incluso si el nombre de tu ruta parece 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 recurso realmente se ajuste a una operación de reemplazo completo o estilo upsert. Usa POST cuando estés creando comandos o acciones. Pero para cualquier mutación que pueda reintentarse a través de los límites de la red, documenta un contrato de idempotencia explícito. Si tus acciones de mutación se desencadenan 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 se debe almacenar una clave de idempotencia
Más tiempo de lo que tu equipo de transporte desea.
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 reenvío manual. Esos números son radicalmente diferentes porque el período de retención correcto es una decisión de negocio, no un valor predeterminado del protocolo.
Si solo mantienes claves durante cinco minutos porque tu cola lo hace, no estás diseñando idempotencia. Estás copiando una limitación de transporte en tu capa de negocio.
Mantén los registros de idempotencia al menos por el máximo de estas ventanas:
- horizonte de reintento del cliente
- horizonte de reenvío de la cola
- horizonte de reproducción del webhook
- horizonte de reproducció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 completamente de acuerdo. No uses marcas de tiempo como clave, porque la desviación del reloj y las colisiones las hacen poco confiables. No almacenes ciegamente las cargas útiles de solicitud completas como el 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 reproducir de forma segura. Si debes reproducir la primera 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 aplican la unicidad en una o más columnas, y INSERT ... ON CONFLICT te permite definir una acción alternativa en lugar de fallar en una violación de unicidad. PostgreSQL también documenta que ON CONFLICT DO UPDATE garantiza un resultado atómico de insertar-o-actualizar bajo concurrencia.
Eso significa que tu capa de idempotencia generalmente debería comenzar 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-entonces-actuar como “seleccionar clave; si falta entonces insertar pedido”. 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 las 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.
Mensajes, eventos y webhooks necesitan su propio límite
¿Cómo manejan los consumidores eventos y mensajes duplicados
Para los consumidores de mensajes, el patrón clásico sigue siendo el correcto. Registra los IDs 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 en el suscriptor y el ID del mensaje para que los duplicados fallen limpiamente y puedan ignorarse.
Muchos equipos llaman a esa tienda explícita processed_messages una tabla de bandeja de entrada. 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 de forma segura.
Una forma minimalista 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. Bueno. La idempotencia debería ser aburrida.
También suele ser mejor que intentar apoyarse en términos de marketing de brokers. El soporte exactamente una vez de Kafka es excelente cuando te quedas dentro del propio modelo transaccional 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. Exactamente una vez de Pub/Sub aún espera que el suscriptor rastree el progreso y evite trabajo duplicado cuando las confirmaciones fallan.
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 de outbox
Si tu servicio actualiza el estado local y también publica un evento, el consumo idempotente por sí solo no es suficiente. También necesitas una forma segura de sacar el evento después de que se confirme la transacción local.
Por eso el patrón de outbox transaccional importa. Chris Richardson describe la idea básica como escribir el evento en una tabla de outbox en la misma transacción que la actualización de negocio, y luego publicarlo asíncronamente. Debezium dice que el patrón de 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 de outbox deduplica los mensajes entrantes y evita registros zombi 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 de outbox en una transacción local.
- Deja que CDC o un despachador de outbox publique el evento.
- Haz que los consumidores aguas abajo también sean idempotentes.
Outbox no elimina la necesidad de consumidores idempotentes. Elimina la necesidad de fingir que una confirmación de base de datos y una publicación de broker pueden ser una transacción distribuida mágica cuando usualmente no pueden.
Los webhooks son solo mensajes con mejor marca
Trata los webhooks entrantes exactamente como mensajes de 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 único de entrega. También nota que las reentregas reutilizan el mismo ID de entrega.
Entonces la arquitectura es directa:
- 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 regresa rápido
Si tu manejador de webhook escribe directamente en 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 motores de flujo de trabajo aún necesitan idempotencia
Las sagas y los motores de flujo de trabajo durables 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 falla antes de informar la finalización, lo que causa que la Actividad se ejecute nuevamente. Temporal también sugiere usar una combinación de ID de Ejecución de Flujo de Trabajo y ID de Actividad como una clave de idempotencia estable al llamar a servicios aguas abajo. Si estás aplicando esto en orquestación de servicios, Microservicios Go para Orquestación de IA/ML cubre los compromisos de flujo de trabajo más amplios.
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 a una tarjeta o desenviar un correo electrónico a posteriori a menos que tu aplicación le brinde pasos idempotentes y compensaciones idempotentes.
Lo mismo se aplica a las sagas. La propia guía de sagas de Temporal describe acciones de compensación que se ejecutan cuando un paso falla. Esas compensaciones también deben ser idempotentes. Si “reembolsar pago” se ejecuta dos veces, puede haber resuelto el error original creando uno nuevo.
Mi regla aquí es brutal y simple. Cada Actividad, cada manejador de comando y cada compensación que toque el mundo exterior debería ser naturalmente idempotente o llevar una clave de idempotencia real al sistema aguas abajo.
Cómo probar la idempotencia antes de la producción
La mayoría de los equipos prueban los caminos felices y luego actúan sorprendidos cuando ocurren reintentos. Eso no es suficiente.
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 falla antes de ack
- un webhook se reproduce con el mismo ID de entrega
- un despachador de outbox publica el mismo evento más de una vez
- una Actividad de flujo de trabajo completa la llamada externa y falla 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 prueba integrales que incluyan solicitudes exitosas, solicitudes fallidas y solicitudes duplicadas. Ese consejo es pedestre y absolutamente correcto.
Añadiría un ejercicio de falla más. Verifica que la respuesta reproducida sea semánticamente equivalente al primer resultado. AWS discute reintentos tardíos y aboga por respuestas que preserven 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 llamante aún tiene un contrato consistente.”
Reglas de opinión que salvan sistemas reales
Aquí están las reglas que aplicaría 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, delimita cada clave por inquilino y operación. Los espacios de claves globales es cómo colisionan solicitudes no relacionadas.
Tercero, persiste la decisión de deduplicación de forma atómica con la mutación. Si eso no es cierto, el diseño es incorrecto.
Cuarto, rechaza reintentos de misma clave y diferente carga útil. Stripe y AWS hacen esto por buenas razones.
Quinto, mantén las claves para el horizonte completo de reproducción del proceso de negocio, no para la ventana de cola más corta.
Sexto, combina productores con un outbox y consumidores con seguimiento de ID de mensaje. Un lado sin el otro es la mitad de un diseño.
Séptimo, propaga la misma identidad de operación aguas abajo 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, bueno. La idempotencia es donde la arquitectura optimista encuentra la realidad de producción. No necesitas complejidad en todas partes. Pero dondequiera que los efectos secundarios duplicados perjudicarían el dinero, el estado o la confianza, la idempotencia debería ser una parte de primera clase del contrato.