Idempotencia y seguridad de reintentos en operaciones REST

Capítulo 7

Tiempo estimado de lectura: 10 minutos

+ Ejercicio

Qué significa idempotencia en la práctica

Una operación es idempotente si ejecutarla una o varias veces produce el mismo efecto observable en el servidor (y, idealmente, la misma respuesta semántica). En APIs reales, la idempotencia se vuelve crítica porque los clientes y las infraestructuras (SDKs, balanceadores, gateways) pueden reintentar solicitudes ante fallos transitorios: timeouts, resets de conexión, respuestas 502/503, etc.

La pregunta clave no es “¿el cliente reintentará?”, sino “¿qué pasa si reintenta cuando el servidor sí procesó la primera vez pero la respuesta se perdió?”. Ahí aparecen duplicados, cobros dobles y estados inconsistentes.

Idempotencia por método (semántica HTTP)

  • GET: idempotente. Repetirlo no debería cambiar estado. Puede haber efectos colaterales no funcionales (logs, métricas), pero no cambios de negocio.
  • PUT: idempotente si representa “establecer el recurso a este estado”. Enviar el mismo cuerpo varias veces deja el recurso igual.
  • DELETE: idempotente en el sentido de que, tras el primer borrado, repetirlo no debería borrar “más”. Puede responder 204 siempre, o 404 si el recurso ya no existe, según contrato.
  • POST: no necesariamente idempotente. Suele representar “crear un nuevo subrecurso” o “ejecutar una acción”, y repetirlo puede crear duplicados o ejecutar la acción dos veces.

Idempotencia por diseño (más allá del método)

Aunque un método sea “idempotente por definición”, tu implementación puede romperlo. Ejemplos comunes:

  • Un PUT que incrementa un contador (“sumar 1”) en vez de establecer un estado.
  • Un DELETE que dispara un cobro de cancelación cada vez que se llama.
  • Un GET que marca “leído” o consume un cupón.

La idempotencia útil para reintentos es la que garantiza que el mismo intento lógico no se ejecute dos veces, incluso si el cliente repite la petición.

Problemas reales: por qué los reintentos rompen sistemas

Timeouts y “procesado pero no respondido”

Escenario típico: el cliente envía una petición, el servidor la procesa, pero la respuesta no llega (timeout, corte de red). El cliente reintenta. Si la operación no es idempotente, el servidor ejecuta de nuevo.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

Cliente --POST /payments--> Servidor (procesa y cobra) --X--> (respuesta se pierde) Cliente (timeout) Cliente --reintento POST /payments--> Servidor (cobra otra vez)

Duplicados en creaciones

Un POST /orders sin protección puede crear dos pedidos idénticos ante reintentos. Esto se agrava si el cliente no tiene una forma fiable de saber si el primer pedido se creó.

Cobros dobles y acciones no reversibles

Operaciones con efectos externos (pasarelas de pago, envío de emails, emisión de facturas, reservas) son especialmente sensibles. Incluso si tu base de datos deduplica, el proveedor externo puede haber recibido dos solicitudes.

Estrategias para reintentos seguros

1) Claves de idempotencia (Idempotency-Key)

La estrategia más común para hacer POST seguro ante reintentos es exigir (o soportar) una cabecera Idempotency-Key generada por el cliente. La idea: el servidor guarda el resultado del primer procesamiento asociado a esa clave y, si llega un reintento con la misma clave, devuelve el mismo resultado sin ejecutar de nuevo.

Qué debe garantizar el servidor

  • Unicidad por ámbito: la clave debe ser única por cliente/tenant y por operación. Normalmente se combina: (client_id, idempotency_key).
  • Repetición consistente: si llega la misma clave con el mismo payload, se devuelve la misma respuesta (código y cuerpo) que la primera vez.
  • Detección de conflicto: si llega la misma clave con un payload distinto, responder con error (p. ej., 409) para evitar ambigüedad.
  • Persistencia temporal: almacenar la clave y el resultado durante una ventana (TTL) suficiente para cubrir reintentos.

Ejemplo de contrato

POST /payments HTTP/1.1 Idempotency-Key: 7f3b2c1a-0b1f-4c3a-9d2e-2f6c9f0d1a11 Content-Type: application/json { "orderId": "ord_123", "amount": 4990, "currency": "EUR", "paymentMethod": "pm_abc" }

Primera vez (procesa):

HTTP/1.1 201 Created Content-Type: application/json { "paymentId": "pay_789", "status": "authorized" }

Reintento (no reprocesa, reusa resultado):

HTTP/1.1 201 Created Content-Type: application/json { "paymentId": "pay_789", "status": "authorized" }

Flujo recomendado paso a paso (cliente y servidor)

Paso 1: el cliente genera la clave

  • Genera un UUID aleatorio por intento lógico (no por request HTTP).
  • Reusa la misma clave en todos los reintentos de esa operación.

Paso 2: el servidor “reserva” la clave de forma atómica

Antes de ejecutar la lógica de negocio, el servidor intenta crear un registro de idempotencia. Si ya existe, decide si devuelve el resultado almacenado o un estado “en progreso”.

CampoDescripción
client_idIdentidad del consumidor (token, API key, tenant)
idempotency_keyClave enviada por el cliente
request_hashHash del cuerpo + parámetros relevantes
statusIN_PROGRESS | COMPLETED | FAILED
response_codeCódigo HTTP devuelto en la primera ejecución
response_bodyRespuesta serializada (o referencia)
created_atMomento de creación
expires_atTTL para purga

Paso 3: ejecutar la operación y guardar el resultado

  • Si el registro estaba “reservado” por esta petición, ejecuta la lógica.
  • Al finalizar, guarda COMPLETED y la respuesta.
  • Si falla, decide si guardas FAILED con detalle (para repetir el mismo fallo) o si permites reintento (ver nota abajo).

Paso 4: reintentos

  • Si llega la misma clave y el request_hash coincide: devuelve el resultado almacenado.
  • Si llega la misma clave pero el hash difiere: devuelve 409 (conflicto de reutilización).
  • Si está IN_PROGRESS: puedes devolver 409/425 o 202 con un recurso de estado (depende del contrato), pero lo importante es no ejecutar de nuevo.

Nota práctica: ¿guardar fallos o permitir reintento?

Si guardas el fallo como resultado idempotente, los reintentos devolverán el mismo error aunque el fallo fuese transitorio. Alternativa: marcar FAILED solo para fallos “deterministas” (validación, reglas de negocio) y permitir reintentos para fallos transitorios (timeouts a dependencias), manteniendo la protección contra duplicados con una ventana de “en progreso” y/o reintentos controlados en el servidor.

2) Deduplicación por cliente (idempotencia a nivel de dominio)

Además (o en lugar) de Idempotency-Key, puedes deduplicar usando un identificador de negocio que el cliente ya tiene: por ejemplo, clientOrderId, externalReference, merchantPaymentId. La regla: ese identificador debe ser único por cliente.

Ejemplo:

POST /orders { "clientOrderId": "web-2026-000991", "items": [...] }

En el servidor:

  • Crear un índice único por (client_id, clientOrderId).
  • Si llega un duplicado, devolver el pedido existente en vez de crear otro.

Ventaja: el identificador tiene significado de negocio y puede ser reutilizado para conciliación. Desventaja: no siempre existe un ID natural, y puede ser más difícil de imponer en todos los clientes.

3) Diseñar POST para crear con identificador aportado

Una forma muy efectiva de evitar duplicados es permitir que el cliente aporte el identificador del recurso a crear, y usar una operación idempotente para “crear o reemplazar” ese recurso. Dos variantes comunes:

Variante A: PUT con ID del cliente

PUT /orders/{orderId} { "items": [...], "shippingAddress": {...} }

Si el cliente reintenta el mismo PUT, el recurso queda igual. Esto funciona bien cuando el cliente puede generar un ID único (UUID) y el recurso es “establecer estado”.

Variante B: POST que acepta un ID explícito

Si por contrato necesitas POST, puedes aceptar un campo id o externalId y deduplicar por él. Aun así, suele ser recomendable acompañarlo de Idempotency-Key para cubrir casos donde el payload cambia o hay acciones colaterales.

Ejemplos de flujos de reintento

Flujo 1: creación de pedido con Idempotency-Key

1) Cliente genera key K 2) Cliente POST /orders (K) 3) Servidor crea registro idempotencia (client, K) = IN_PROGRESS 4) Servidor crea order O123 5) Servidor guarda respuesta asociada a (client, K) = COMPLETED + 201 + body 6) Respuesta se pierde 7) Cliente reintenta POST /orders (K) 8) Servidor detecta COMPLETED y devuelve la misma respuesta (O123)

Flujo 2: reintento mientras está en progreso

1) Cliente POST /payments (K) 2) Servidor marca IN_PROGRESS 3) Servidor llama a pasarela (tarda) 4) Cliente timeout y reintenta (K) 5) Servidor ve IN_PROGRESS y NO ejecuta otra vez 6) Servidor responde 202 (o 409/425 según contrato) indicando que está procesando 7) Cuando termina, futuras llamadas con (K) devuelven el resultado final

Flujo 3: reutilización incorrecta de la clave

1) Cliente POST /orders (K) body A 2) Servidor guarda request_hash(A) 3) Cliente POST /orders (K) body B 4) Servidor detecta hash distinto y responde 409 Conflict

Cómo registrar y expirar claves de idempotencia

Registro: requisitos de implementación

  • Operación atómica: la inserción del registro de idempotencia debe ser atómica (por ejemplo, con una restricción única en base de datos o un lock distribuido).
  • Hash estable: calcula request_hash sobre los campos relevantes (cuerpo normalizado + parámetros que afecten al resultado). Evita incluir campos no deterministas (timestamps generados por el cliente) si no deben invalidar la idempotencia.
  • Almacenamiento de respuesta: guarda al menos el status code y un cuerpo serializado o una referencia (por ejemplo, el ID del recurso creado). Si el cuerpo es grande, guarda una referencia al recurso y reconstruye la respuesta.
  • Seguridad: no guardes datos sensibles en claro si no es necesario; cifra o minimiza el payload almacenado.

Expiración (TTL): cómo elegirla

La expiración debe cubrir el horizonte de reintentos esperado: reintentos inmediatos (segundos) y reintentos tardíos (minutos u horas) si hay colas o clientes móviles. Una guía práctica:

  • Pagos: TTL más largo (por ejemplo, 24h) para evitar dobles cargos por reintentos tardíos.
  • Creaciones internas: TTL medio (1–6h) suele ser suficiente.
  • Operaciones baratas y reversibles: TTL corto (5–30 min) puede bastar.

Purga y almacenamiento

  • Incluye expires_at y un job de limpieza (o TTL nativo si usas un almacén que lo soporte).
  • Monitorea el crecimiento: las claves son “logs de deduplicación”; dimensiona el almacenamiento según QPS y TTL.
  • Registra métricas: tasa de reintentos, hits de idempotencia (cuántas veces se devolvió un resultado cacheado), conflictos por hash distinto.

Pseudocódigo de referencia (servidor)

function handlePost(request): client = authClient(request) key = request.header["Idempotency-Key"] if not key: return processNormallyOrReject() hash = stableHash(request.body, request.queryRelevant) record = findRecord(client, key) if record exists: if record.request_hash != hash: return 409 if record.status == "COMPLETED": return record.response_code, record.response_body if record.status == "IN_PROGRESS": return 202  # o 409/425 según contrato # no existe: intentar crear (client,key) unique try: insertRecord(client, key, hash, status="IN_PROGRESS", expires_at=now+TTL) except UniqueViolation: # carrera: volver a leer y aplicar lógica anterior return handlePost(request) # ejecutar lógica real result = executeBusinessLogic(request) updateRecord(client, key, status="COMPLETED", response_code=result.code, response_body=result.body) return result

Checklist de diseño para reintentos seguros

  • Identifica endpoints con efectos no reversibles (pagos, envíos, emisión de documentos) y prioriza idempotencia.
  • Para POST críticos, soporta Idempotency-Key y define comportamiento para: repetición, en progreso y conflicto por payload distinto.
  • Considera deduplicación por identificador de negocio (externalReference) cuando exista.
  • Cuando sea viable, permite creación con ID aportado y usa operaciones idempotentes para “establecer estado”.
  • Define TTL, purga, métricas y límites (por ejemplo, tamaño máximo de respuesta almacenada).

Ahora responde el ejercicio sobre el contenido:

Ante reintentos de un POST crítico, ¿qué comportamiento del servidor garantiza idempotencia segura cuando se usa Idempotency-Key?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

La idempotencia para reintentos se logra registrando la clave por cliente y el hash del request: misma clave + mismo payload => misma respuesta sin reprocesar; misma clave + payload distinto => conflicto para evitar ambigüedad.

Siguiente capítulo

Manejo consistente de errores y validación en APIs REST

Arrow Right Icon
Portada de libro electrónico gratuitaDiseño de APIs REST: buenas prácticas, errores comunes y estándares
70%

Diseño de APIs REST: buenas prácticas, errores comunes y estándares

Nuevo curso

10 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.