idempotencia · diseño de APIs · reintentos · timeouts · equipos pequeños · sistemas distribuidos

Claves de Idempotencia y Reintentos: Una Guía Práctica para APIs Pequeñas

Aprende a implementar claves de idempotencia, manejar reintentos y configurar timeouts sin sobreingeniería en tu API pública. Un enfoque conservador para desarrolladores independientes y equipos pequeños.

Publicado:

El Problema: Cobros Duplicados y Llamadas a las 3 AM

Has lanzado un endpoint de pagos. Un usuario hace clic en “Pagar” dos veces, o su red móvil falla, y de repente ha sido cobrado dos veces. Tus logs muestran dos solicitudes POST exitosas a 847 milisegundos de distancia. Soporte al cliente ya está redactando un correo de reembolso. Esto no es una condición de carrera; es martes.

Para APIs públicas pequeñas, la solución no es más código; son mejores contratos. Necesitas claves de idempotencia para operaciones que mutan estado, una estrategia de reintentos con retroceso exponencial y timeouts sensatos. El objetivo es hacer tu API segura para reintentar sin convertir tu base de código en un libro de texto de sistemas distribuidos.

¿Qué es una Clave de Idempotencia?

Una clave de idempotencia es un identificador único (usualmente un UUID v4) que el cliente adjunta a una solicitud para garantizar que realizar la misma operación múltiples veces produzca el mismo resultado solo una vez. El servidor recuerda la clave y devuelve la respuesta en caché en lugar de volver a ejecutar la operación.

En HTTP, algunos métodos son naturalmente idempotentes: GET, PUT y DELETE. POST y PATCH no lo son. Eso significa que cada POST que crea un recurso (pagos, pedidos, suscripciones) debe hacerse seguro mediante un mecanismo de idempotencia explícito.

¿Por qué ocurren los reintentos y por qué importan?

Las redes son poco confiables. Los clientes timeoutean, las redes móviles pierden paquetes y la infraestructura falla. Cuando un cliente no recibe una respuesta, reintenta. Sin idempotencia, cada reintento puede desencadenar un efecto secundario duplicado: un cobro doble, una deducción extra de inventario, un registro duplicado.

Una encuesta de 2024 a más de 400 equipos de backend encontró que el 73% había enviado lógica de idempotencia que fallaba en producción bajo condiciones reales de red móvil. El culpable no es la documentación faltante; es que la mayoría de los equipos implementan claves de idempotencia como implementarían cualquier otra característica: almacenan un hash, lo verifican, listo.

El Error Común: Almacenar Claves sin Bloquear el Estado

Este es el patrón que verás en revisiones de código en producción:

  1. El cliente envía POST /payments con Idempotency-Key: abc123.
  2. El servidor verifica si abc123 existe en el almacenamiento.
  3. Si no existe, procesa el pago y almacena la clave.
  4. Si existe, devuelve la respuesta en caché.

Se ve limpio. Es stateless. Y es incorrecto.

El fallo ocurre cuando el cliente reintenta antes de que el paso 4 complete. Un tropiezo de red, un cold start de Lambda, una consulta lenta a la base de datos; no importa. Dos solicitudes con abc123 llegan simultáneamente. Ambas pasan la verificación “¿He visto esto?”. Ambas cargan la tarjeta.

No solo estás verificando si una clave existe. Estás verificando si el procesamiento ha comenzado, está en curso o ha terminado. Un flag booleano no puede capturar eso. Una marca de tiempo tampoco. Necesitas un bloqueo atómico que prevenga la ejecución concurrente mientras rastrea el resultado.

Cómo Implementar Correctamente: Bloqueos Atómicos y Rastreo de Estado

Una implementación robusta trata la clave de idempotencia como una máquina de estados con tres estados: pending, success y failure.

Cuando llega una solicitud:

  1. Busca la clave.
  2. Si está success o failure, devuelve la respuesta en caché inmediatamente.
  3. Si está pending o ausente, adquiere un bloqueo atómico (por ejemplo, un SET NX de Redis o un bloqueo de fila en la base de datos) y transiciona el estado a pending.
  4. Ejecuta la operación.
  5. En éxito, almacena la respuesta y marca la clave como success.
  6. En fallo, marca la clave como failure y devuelve un error.
  7. Libera el bloqueo.

Esto asegura que solo una solicitud con una clave dada se procese a la vez. Los reintentos concurrentes esperarán el bloqueo y luego devolverán el resultado en caché.

Compensaciones: Simplicidad vs. Correctitud

No siempre necesitas una máquina de estados completa. El nivel correcto de complejidad depende de tu tráfico y riesgo.

Mantén la simplicidad si:

Invierte en correctitud si:

Un punto medio es almacenar la clave con un estado y una respuesta, pero usar una restricción única en la base de datos para prevenir procesamiento concurrente. Si dos solicitudes llegan simultáneamente, la segunda fallará la restricción única y puedes devolver un 409 Conflict. Sin embargo, como se discutió en Hacker News, un 409 no le dice al cliente nada sobre si la solicitud original fue exitosa o falló. El cliente debe entonces decidir si reintenta o pregunta al usuario. Para la mayoría de las APIs pequeñas, esa es una compensación aceptable—si la documentas claramente.

Pasos Prácticos para una API Pública Pequeña

  1. Identifica endpoints que mutan. Cada POST, PATCH o DELETE que cree o cambie estado debe soportar claves de idempotencia.
  2. Requiere el header. Usa Idempotency-Key (o Idempotency-Request-Id) en el header de la solicitud. Rechaza solicitudes sin él para endpoints críticos.
  3. Elige almacenamiento. Redis con TTL es rápido y simple. Una base de datos relacional con un índice único y una columna de estado también funciona. Elige lo que ya uses.
  4. Establece una expiración. Las claves no deben vivir para siempre. 24 horas es un default común; ajústalo según tu lógica de negocio.
  5. Maneja solicitudes concurrentes. Usa un bloqueo atómico (Redis SET NX, bloqueos advisory de la base de datos o concurrencia optimista) para asegurar que solo una solicitud por clave se procese a la vez.
  6. Implementa reintentos en el cliente. Asesora a tus consumidores de API para usar retroceso exponencial con jitter. Nunca reintentes métodos idempotentes (GET, PUT, DELETE) sin una clave; ya son seguros.
  7. Configura timeouts. Define timeouts claros para tu API (por ejemplo, 5 segundos para la mayoría de operaciones, 30 segundos para cómputos pesados). Los timeouts previenen que solicitudes de larga duración mantengan bloqueos indefinidamente.
  8. Documenta todo. Dile a los desarrolladores cómo usar las claves de idempotencia, qué headers enviar, qué respuestas esperar y cómo deben funcionar los reintentos. Un documento de API bien escrito es tu primera línea de defensa.

FAQ

P: ¿Necesito idempotencia para solicitudes GET? R: No. GET es idempotente por definición; llamarlo múltiples veces tiene el mismo efecto que llamarlo una vez. Sin embargo, aún deberías considerar el caching para evitar trabajo duplicado.

P: ¿Qué pasa si el cliente envía un payload diferente con la misma clave de idempotencia? R: Ese es un error del cliente. Tu API debe rechazar la solicitud con un 400 Bad Request o 422 Unprocessable Entity. La clave debe mapear a una única operación específica.

P: ¿Cuánto tiempo debo almacenar las claves de idempotencia? R: Mientras tome para que un cliente reintente razonablemente. 24 horas cubre la mayoría de escenarios de red móvil. Una retención más larga aumenta costos de almacenamiento y riesgo de privacidad; una más corta arriesga duplicados.

P: ¿Puedo usar una restricción única de la base de datos en lugar de Redis? R: Sí. Una restricción única en la columna de clave de idempotencia previene inserts duplicados. Combínala con una columna de estado y una transacción para rastrear el estado. Esto es más simple y evita una dependencia extra, pero puede ser más lento bajo alta concurrencia.

P: ¿Debo devolver 409 Conflict o replay la respuesta de éxito en una clave duplicada? R: Depende de tu modelo de amenaza. Si quieres forzar al cliente a manejar conflictos explícitamente, devuelve 409. Si quieres ocultar complejidad y asegurar que el cliente reciba una respuesta consistente, replay la respuesta en caché. La mayoría de las APIs grandes replay el éxito; las APIs pequeñas pueden elegir cualquiera, pero documenta el comportamiento claramente.

Conclusión

La idempotencia no es un lujo; es una necesidad para cualquier API que maneje reintentos. Para equipos pequeños, el objetivo es implementarla correctamente sin sobreingeniería. Usa bloqueos atómicos, establece timeouts sensatos, almacena claves con expiración y documenta tus elecciones. Tus usuarios (y tu horario de on‑call) te lo agradecerán.

Fuentes