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
PUTque incrementa un contador (“sumar 1”) en vez de establecer un estado. - Un
DELETEque dispara un cobro de cancelación cada vez que se llama. - Un
GETque 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.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
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”.
| Campo | Descripción |
|---|---|
client_id | Identidad del consumidor (token, API key, tenant) |
idempotency_key | Clave enviada por el cliente |
request_hash | Hash del cuerpo + parámetros relevantes |
status | IN_PROGRESS | COMPLETED | FAILED |
response_code | Código HTTP devuelto en la primera ejecución |
response_body | Respuesta serializada (o referencia) |
created_at | Momento de creación |
expires_at | TTL 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
COMPLETEDy la respuesta. - Si falla, decide si guardas
FAILEDcon detalle (para repetir el mismo fallo) o si permites reintento (ver nota abajo).
Paso 4: reintentos
- Si llega la misma clave y el
request_hashcoincide: 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 finalFlujo 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 ConflictCó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_hashsobre 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 codey 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_aty 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 resultChecklist de diseño para reintentos seguros
- Identifica endpoints con efectos no reversibles (pagos, envíos, emisión de documentos) y prioriza idempotencia.
- Para
POSTcríticos, soportaIdempotency-Keyy 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).