¿Qué hace que un endpoint sea idempotente, y cómo funcionan las claves de idempotencia en operaciones no idempotentes como los pagos?
La idempotencia es lo que le permite a un cliente reintentar a ciegas después de un timeout sin cobrarle dos veces a nadie: la garantía que HTTP te da gratis, y la que tenés que construir vos.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Qué hace que un endpoint sea idempotente, y cómo funcionan las claves de idempotencia en operaciones no idempotentes como los pagos?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Idempotente, seguro, y por qué la diferencia importa
Una operación es idempotente si ejecutarla una vez produce el mismo resultado en el servidor que ejecutarla dos, diez o mil veces con la misma entrada. No dice nada sobre si la primera llamada tiene un efecto secundario — DELETE /users/42 sin dudas cambia el estado la primera vez — solo promete que repetir exactamente la misma petición después no acumula ese efecto. Es una garantía más angosta que segura (sin efectos secundarios en absoluto, como GET), y es fácil confundir ambos conceptos. PUT /users/42 con una representación completa es idempotente (reemplazar un recurso con el mismo valor dos veces deja todo igual que reemplazarlo una sola vez) pero no es seguro (sí escribe).
| Método | Seguro | Idempotente | Por qué |
|---|---|---|---|
GET, HEAD, OPTIONS | Sí | Sí | Solo lectura por definición |
PUT | No | Sí | Reemplazo completo — aplicar la misma representación dos veces es un no-op la segunda vez |
DELETE | No | Sí | Borrar un recurso ya borrado sigue resultando en "no existe" |
POST | No | No | Crea un recurso nuevo por defecto — llamarlo dos veces crea dos |
PATCH | No | Depende | Un patch de reemplazo completo es idempotente; {"op": "increment", "amount": 5} no lo es |
#Dónde se rompe esto en el mundo real
HTTP te da la garantía de idempotencia de PUT y DELETE gratis — el servidor es responsable de implementarlos así, y si lo hace, un cliente puede reintentar a ciegas después de un timeout sin ningún riesgo. El problema es que la operación más propensa a reintentos en un backend típico — "crear algo" — es un POST, y POST no trae esa garantía. En ningún lado esto es más peligroso que en pagos: un cliente manda POST /payments, la tarjeta se cobra, y la respuesta se pierde por un corte de red antes de llegar al cliente. Desde el punto de vista del cliente, la petición pudo haber fallado. El único instinto seguro — reintentar — ahora arriesga cobrarle dos veces al cliente por la misma compra.
#Claves de idempotencia: haciendo que POST se comporte
Una clave de idempotencia es un token único generado por el cliente — típicamente un UUID — que se envía como header (Idempotency-Key) junto a una petición que naturalmente no es idempotente. El contrato del servidor pasa a ser: "la primera vez que vea esta clave, voy a ejecutar la operación de verdad y guardar el resultado; cada petición posterior con la misma clave recibe ese mismo resultado guardado, sin tocar el mundo de nuevo." El cliente es responsable de reutilizar exactamente la misma clave en los reintentos de la misma operación lógica, y de generar una clave nueva para una operación genuinamente distinta.
Del lado del servidor hacen falta tres cosas para hacerlo bien:
- Reserva atómica. Dos peticiones con la misma clave pueden llegar en simultáneo (un cliente que hace timeout y reintenta de inmediato, compitiendo con su propia petición original). El servidor debe reclamar la clave atómicamente antes de hacer cualquier trabajo — un
INSERT ... ON CONFLICT DO NOTHINGen Postgres, o unSET key value NXen Redis — para que exactamente un llamador avance y al otro se le diga que espere o se le devuelva el resultado eventual. - Huella digital del payload. Guardar un hash del cuerpo de la petición junto con la clave. Si la misma clave llega con un cuerpo distinto, eso es un bug del cliente (o un actor malicioso), no un reintento legítimo — hay que rechazarlo en vez de devolver silenciosamente un resultado viejo para la petición equivocada.
- Un cache de respuesta con un TTL razonable. Guardar el código de estado y el cuerpo de la operación completada para que los reintentos puedan repetirlo tal cual, y hacerlo expirar en una ventana acotada (24 a 72 horas es típico) en vez de para siempre, porque las claves de idempotencia están pensadas para cubrir la ventana de reintento del cliente, no para convertirse en un libro contable permanente.
import jsonimport hashlibimport redisfrom flask import Flask, request, jsonifyapp = Flask(__name__)r = redis.Redis(host="localhost", port=6379, db=0)IDEMPOTENCY_TTL_SECONDS = 24 * 60 * 60def idempotent(view_func):def wrapper(*args, **kwargs):key = request.headers.get("Idempotency-Key")if not key:return jsonify({"error": "Idempotency-Key header is required"}), 400body_hash = hashlib.sha256(request.get_data()).hexdigest()store_key = f"idem:{key}"reserved = r.set(store_key,json.dumps({"status": "in_progress", "body_hash": body_hash}),nx=True,ex=IDEMPOTENCY_TTL_SECONDS,)if not reserved:existing = json.loads(r.get(store_key))if existing.get("body_hash") != body_hash:return jsonify({"error": "Idempotency-Key reused with a different body"}), 422if existing.get("status") == "in_progress":return jsonify({"error": "Original request is still processing"}), 409return jsonify(existing["response_body"]), existing["status_code"]try:status_code, response_body = view_func(*args, **kwargs)except Exception:r.delete(store_key)raiser.set(store_key,json.dumps({"status": "completed","body_hash": body_hash,"status_code": status_code,"response_body": response_body,}),ex=IDEMPOTENCY_TTL_SECONDS,)return jsonify(response_body), status_codewrapper.__name__ = view_func.__name__return wrapper@app.route("/payments", methods=["POST"])@idempotentdef create_payment():data = request.get_json()payment = charge_card(data["card_token"], data["amount_cents"])return 201, {"id": payment.id, "status": payment.status}
Un cliente que llama a este endpoint manda la clave él mismo:
POST /payments HTTP/1.1Host: api.example.comIdempotency-Key: 6c1f1c9e-df7a-4b1a-9e2b-6a2b6a9b6e10Content-Type: application/json{ "card_token": "tok_abc123", "amount_cents": 4999 }
🧠 Check your understanding
0/3 · 0/3 answered1. ¿Qué par de métodos HTTP son idempotentes pero NO seguros?
2. La petición POST /payments de un cliente hace timeout pero el cobro sí se procesó en el servidor. El cliente reintenta con la MISMA Idempotency-Key. ¿Qué debería hacer el servidor?
3. ¿Por qué el servidor debe guardar un hash del cuerpo de la petición junto con la clave de idempotencia?