En sistemas distribuidos el fallo no es la excepción: es el estado normal. Las redes caen, los timeouts aparecen y los clientes reintentan. El problema no es el reintento en sí, sino que un reintento sobre una operación que ya se ejecutó puede crear un cargo duplicado, una suscripción doble o un email enviado dos veces. La idempotencia es la propiedad que hace que repetir una operación sea inofensivo.
Una operación es idempotente si aplicarla varias veces produce el mismo resultado que aplicarla una sola vez. GET y PUT suelen serlo por diseño, pero los POST que cambian estado casi nunca lo son — y ahí es donde vive el riesgo.
1. Deja de perseguir la entrega perfecta
La regla práctica de 2026 es simple: el delivery es at-least-once (al menos una vez), no exactly-once. Acepta que los mensajes pueden duplicarse y convierte la repetición en algo inocuo. Cada POST o PATCH que cambia estado acepta una idempotency key; GET, PUT y DELETE no la necesitan.
2. Idempotency-Key: el patrón canónico
El cliente genera un UUID y lo envía en la cabecera Idempotency-Key. El servidor registra el resultado de la primera ejecución asociado a esa clave y, ante un reintento con la misma clave, devuelve la respuesta guardada en lugar de volver a procesar.
// Middleware de idempotencia en Express
const store = new Map(); // en producción: Redis con TTL
async function idempotency(req, res, next) {
const key = req.header('Idempotency-Key');
if (!key) return res.status(400).json({ error: 'Idempotency-Key requerida' });
const prev = store.get(key);
if (prev) {
res.set('Idempotency-Replay', 'true');
return res.status(prev.status).json(prev.body);
}
const originalJson = res.json.bind(res);
res.json = (body) => {
store.set(key, { status: res.statusCode, body, ts: Date.now() });
return originalJson(body);
};
next();
}
El detalle crítico: la respuesta se debe guardar después de una ejecución exitosa, y el reintento debe devolver el mismo status y cuerpo. La cabecera Idempotency-Replay: true le dice al cliente que recibió una respuesta cacheada.
3. El cliente también tiene responsabilidad
Un reintento seguro reutiliza la misma clave y solo reintenta ante errores transitorios (5xx o timeout). Los 4xx no se reintentan: indican un problema de la petición, no de la red.
// Cliente con retry idempotente (at-least-once)
async function charge(card, amount, idemKey) {
for (let attempt = 0; attempt < 3; attempt++) {
try {
return await post('/v1/charges', { card, amount }, {
headers: { 'Idempotency-Key': idemKey }
});
} catch (err) {
if (err.status >= 500) await sleep(2 ** attempt * 200);
else throw err; // 4xx no se reintenta
}
}
} - Backoff exponencial: espera creciente entre intentos para no saturar el servicio.
- Jitter: añade ruido aleatorio para evitar la "tormenta de reintentos" sincronizados.
- Clave estable: la misma operación de negocio siempre usa la misma clave, aunque el transporte falle.
4. ¿Dónde guardar el estado?
El almacén de idempotencia debe ser rápido, compartido entre instancias y con expiración. Redis con TTL es la opción habitual: la clave vive el tiempo suficiente para cubrir la ventana de reintentos y luego se libera sola.
# Redis: expiración automática de la entrada
SET idem:9f2c-a1b8 "{\"status\":201,\"body\":{...}}" EX 86400
# La clave expira en 24h; cubre el window de
# reintentos del cliente sin crecer indefinidamente. 5. Anti-patrones frecuentes
- Clave por petición: generar un UUID nuevo en cada reintento anula la deduplicación.
- TTL corto: si expira antes del último reintento, duplicas la operación.
- Guardar antes de confirmar: bloquea reintentos válidos tras un fallo parcial.
- Ignorar concurrencia: dos llamadas con la misma clave en paralelo pueden ambas procesar; usa un lock o upsert atómico.
Resumen
La idempotencia no es una optimización: es la base de una API que sobrevive a la red real. Acepta el at-least-once, firma cada operación con una clave estable y haz que el servidor recuerde el resultado. Así los reintentos dejan de ser una fuente de incidentes y se convierten en una red de seguridad.