Lógica de Reintentos en APIs y Backoff Exponencial: Patrones que Funcionan

Dominar el retroceso exponencial con fluctuación completa, encabezados Retry-After, claves de idempotencia y disyuntores, y luego prueba la lógica de reintentos de tu API con simulaciones de Apidog.

INEZA Felin-Michel

INEZA Felin-Michel

31 August 2026

Lógica de Reintentos en APIs y Backoff Exponencial: Patrones que Funcionan

Apidog para empresas

Despliegue local

SSO & RBAC

Conforme con SOC 2

Explorar Apidog Enterprise

Tu llamada a la API de pago falló a las 2 a.m. ¿Fue una interrupción de red, un límite de tasa o un servidor caído? La respuesta decide si reintentar guarda la transacción o si cobra doble a un cliente.

Los reintentos son el patrón de resiliencia más común en sistemas distribuidos, y el que más se estropea. Un bucle alrededor de una llamada HTTP parece programación defensiva. Mal hecho, convierte una interrupción de 30 segundos en una de 30 minutos, porque miles de clientes atacan un servidor en apuros al mismo tiempo. Bien hecho, los reintentos absorben fallas transitorias tan limpiamente que tus usuarios nunca las notan.

Esta guía cubre la lógica de reintentos de la que dependen los sistemas de producción: qué códigos de estado reintentar, la fórmula de retroceso exponencial con fluctuación completa (full jitter), encabezados Retry-After, claves de idempotencia, presupuestos de reintentos y disyuntores. También verás cómo probar que tu cliente se comporta correctamente simulando 429 y 503 con servidores de prueba de Apidog, porque un patrón de reintentos que nunca has probado contra un servidor fallido es una suposición, no un diseño. Los equipos que construyen lógica de reintentos de API fintech aprenden esto de la manera costosa; tú no tienes por qué hacerlo.

Por qué los reintentos ingenuos empeoran las interrupciones

Imagina un servicio que maneja 1,000 solicitudes por segundo. Tiene un problema durante cinco segundos. Cada cliente reintenta inmediatamente, tres veces cada uno. Tus 1,000 solicitudes por segundo de demanda se convierten en 4,000 solicitudes por segundo dirigidas a un servidor que ya está de rodillas. Se cae por completo. Ahora cada cliente reintenta de nuevo.

Ese ciclo de retroalimentación tiene un nombre: una tormenta de reintentos. La estampida sincronizada cuando el servidor vuelve a la vida es la "manada atronadora" (thundering herd). El libro SRE de Google menciona este patrón en su capítulo sobre abordar fallas en cascada: los reintentos sin retroceso amplifican la carga exactamente cuando el sistema menos puede permitírselo, y pueden mantener un servicio caído mucho después de que se haya solucionado la falla original.

Dos fallas de diseño causan la mayoría de las tormentas de reintentos:

La solución no es "nunca reintentar". La solución es reintentar selectivamente, con retrasos aleatorios crecientes y con un límite estricto sobre cuánta carga adicional añaden tus reintentos.

Reintenta estas fallas, nunca aquellas

Antes de cualquier cálculo de retroceso, tu cliente necesita una tabla de decisiones. Reintentar una solicitud que el servidor ya ha rechazado como inválida desperdicia capacidad y contamina los registros. Reintentar una falla transitoria es el objetivo principal.

Reintenta estas:

Señal Significado
429 Too Many Requests Alcanzaste un límite de tasa. Retrocede y vuelve más lento.
502 Bad Gateway Un salto intermedio (upstream) devolvió datos basura. A menudo transitorio.
503 Service Unavailable El servidor está sobrecargado o reiniciándose.
504 Gateway Timeout Una dependencia upstream fue demasiado lenta.
Reinicios de conexión, fallas de DNS, tiempos de espera de socket Es posible que la solicitud nunca haya llegado.

Un tiempo de espera de puerta de enlace 504 merece una atención especial: el origen puede haber procesado tu solicitud aunque la puerta de enlace dejó de esperar. Esa distinción es importante cuando llegamos a la idempotencia.

Nunca reintentes estas:

Señal Significado
400 Bad Request Tu carga útil está mal formada. También lo estará la próxima vez.
401 Unauthorized Tus credenciales son incorrectas o han caducado. Actualiza el token, no hagas un bucle.
403 Forbidden No tienes permiso. Reintentar no lo concederá.
422 Unprocessable Entity La validación falló. Corrige los datos, no el momento.

La regla: reintenta cuando la falla se relaciona con el estado del servidor o la red. Falla rápidamente cuando la falla se relaciona con tu solicitud. Un 429 se encuentra en el medio; es reintentable, pero también es una señal de que tu tasa de solicitud general necesita ser trabajada, lo cual es un problema de limitación de tasa a resolver aguas arriba de cualquier bucle de reintentos.

La fórmula de retroceso exponencial y por qué es importante el "jitter"

El retroceso exponencial significa que cada reintento espera más tiempo que el anterior, duplicándose por defecto:

delay = base * 2^retry_count

Con una base de 500 ms, eso es 0.5s, 1s, 2s, 4s, 8s. Añade un límite (por ejemplo, 30 segundos) para que los retrasos no se conviertan en minutos:

delay = min(cap, base * 2^retry_count)

Esto resuelve el problema del "bombardeo" pero no el de la sincronización. Si 5,000 clientes fallan en el mismo instante, el retroceso exponencial simple hará que todos los 5,000 regresen en t=0.5s, luego en t=1s, luego en t=2s. Siguen siendo oleadas. Sigue siendo una manada, solo que más educada.

El "jitter" rompe la sincronización al aleatorizar el retraso. El Blog de Arquitectura de AWS analizó los números en su análisis de retroceso exponencial y jitter, simulando clientes en competencia contra un recurso disputado. El retroceso sin jitter seguía produciendo picos agrupados de llamadas. El "full jitter" (fluctuación completa), que elige un retraso aleatorio entre cero y el techo exponencial, produjo tanto el menor número total de llamadas como tiempos de finalización cercanos a los más cortos:

delay = random_between(0, min(cap, base * 2^retry_count))

Ese resultado sorprende a la gente. Aleatorizar hasta cero parece descuidado en comparación con un programa de duplicación ordenado. Pero distribuir los clientes uniformemente a lo largo de la ventana es exactamente lo que mantiene la carga del servidor plana. El análisis de AWS también probó el "equal jitter" (mitad fijo, mitad aleatorio) y el "decorrelated jitter"; el "full jitter" y el "decorrelated jitter" salieron ganando, y el "full jitter" es el más sencillo de escribir correctamente. Úsalo como tu patrón de reintentos predeterminado a menos que tengas mediciones que digan lo contrario.

Respeta Retry-After cuando el servidor te lo indique

El retroceso es tu cliente adivinando cuánto tiempo debe esperar. A veces el servidor elimina las conjeturas. El encabezado Retry-After, definido para respuestas 429 y 503, contiene un número de segundos o una fecha HTTP:

HTTP/1.1 429 Too Many Requests
Retry-After: 12

Cuando este encabezado está presente, anula tu retroceso calculado. El servidor sabe cuándo se restablece su ventana de límite de tasa o cuándo termina su mantenimiento; tu programación exponencial no lo sabe. Que los clientes ignoren Retry-After es una de las razones por las que los proveedores escalan de la limitación a las prohibiciones directas. Analízalo, respétalo y aún así aplica tu límite y el número máximo de reintentos para que un Retry-After: 86400 hostil o con errores no pueda colgar a tu trabajador durante un día.

Idempotencia: la precondición para reintentar POST

Aquí está la trampa en ese 504 de antes. GET, PUT y DELETE son idempotentes por contrato: enviarlos dos veces deja el sistema en el mismo estado. POST no lo es. Si POST /v1/payments expira después de que el servidor lo procesó, tu reintento crea un segundo pago. Felicidades, has construido una máquina de doble cobro con una excelente disponibilidad.

La solución es una clave de idempotencia: una ID única generada por el cliente (generalmente un UUID) enviada como encabezado en cada operación lógica. El servidor almacena la clave con la primera respuesta y reproduce esa respuesta almacenada para cualquier duplicado. Las solicitudes idempotentes de Stripe funcionan exactamente de esta manera, y la mayoría de las API de pago y aprovisionamiento han seguido el ejemplo.

Dos reglas hacen que las claves funcionen:

Si la API a la que llamas no admite claves de idempotencia, no reintentes automáticamente las escrituras no idempotentes. Muestra el fallo y deja que un humano o un trabajo de conciliación decida.

Presupuestos de reintentos y disyuntores: la vía de escape

El retroceso da forma a cuándo ocurren los reintentos. No limita cuántos ocurren. Durante una interrupción prolongada, incluso los clientes bien fluctuados acumulan carga de reintentos, y los reintentos en capas se multiplican: si tu puerta de enlace API reintenta 3 veces y tu cliente de servicio reintenta 3 veces, un clic de usuario puede convertirse en 9 solicitudes.

Dos mecanismos limitan el daño:

Presupuestos de reintentos. En lugar de "3 reintentos por solicitud", impone "los reintentos pueden añadir como máximo un 10% de carga adicional", medido en una ventana deslizante. Cuando se agota el presupuesto, los fallos se devuelven inmediatamente. Esto mantiene acotada la amplificación de reintentos, sin importar cuántas solicitudes estén fallando a la vez. Linkerd y Envoy ofrecen esto como una configuración de primera clase.

Disyuntores (Circuit breakers). Rastrea la tasa de fallos por dependencia (downstream). Cuando supera un umbral, el disyuntor se abre: las llamadas fallan instantáneamente sin tocar la red. Después de un período de enfriamiento, unas pocas solicitudes de prueba verifican si la dependencia se recuperó antes de que el disyuntor se cierre de nuevo. Donde el retroceso ralentiza educadamente la estampida, el disyuntor la cancela. Cada diseño serio de reintentos empareja los dos, porque el retroceso por sí solo sigue enviando cada solicitud eventualmente.

Un ejemplo listo para producción en Python

Aquí tienes el patrón completo en un solo lugar: filtrado de estado reintentable, "full jitter", soporte para Retry-After, una clave de idempotencia y un límite estricto de reintentos.

import random
import time
import uuid
import requests

RETRYABLE = {429, 502, 503, 504}
BASE = 0.5     # segundos
CAP = 30.0     # límite superior para cualquier retraso individual
MAX_RETRIES = 5

def create_payment(payload):
    idempotency_key = str(uuid.uuid4())  # una clave por pago lógico
    headers = {"Idempotency-Key": idempotency_key}

    for retry_count in range(MAX_RETRIES + 1):
        try:
            resp = requests.post(
                "https://api.acmepay.com/v1/payments",
                json=payload, headers=headers, timeout=10,
            )
            if resp.status_code < 400:
                return resp.json()
            if resp.status_code not in RETRYABLE:
                resp.raise_for_status()  # 400/401/403/422: falla rápido
            retry_after = resp.headers.get("Retry-After")
        except (requests.ConnectionError, requests.Timeout):
            retry_after = None  # falla de red: pasa a retroceso

        if retry_count == MAX_RETRIES:
            raise RuntimeError("el pago falló después de todos los reintentos")

        if retry_after and retry_after.isdigit():
            delay = min(CAP, float(retry_after))
        else:
            delay = random.uniform(0, min(CAP, BASE * 2 ** retry_count))
        time.sleep(delay)

Cabe destacar: la clave se genera una sola vez, fuera del bucle. Retry-After prevalece sobre el retroceso calculado, pero sigue respetando el límite. Los estados no reintentables se elevan inmediatamente. Si estás en el lado de JavaScript, la librería axios-retry te da la misma forma con los ganchos retryCondition y retryDelay; la tabla de decisiones sigue siendo idéntica.

Cómo probar el comportamiento de reintentos antes de que la producción lo haga por ti

La mayoría de los equipos implementan código de reintentos que nunca ha ejecutado su rama de error. El "camino feliz" se probó; el camino del 503 se ejecuta por primera vez durante una interrupción real. Puedes hacerlo mejor con dos características de Apidog.

Simula fallas con servidores mock. El mock inteligente de Apidog te permite definir un endpoint como /v1/payments y programar sus respuestas. Haz que devuelva 503 para las dos primeras llamadas y 200 en la tercera, o que devuelva un 429 con Retry-After: 5, o añade un retraso de 15 segundos para activar el tiempo de espera de tu cliente. Apunta tu cliente a la URL del mock y observa cómo el bucle de reintentos maneja cada escenario, sin necesidad de un incidente en producción.

Afirma el comportamiento del cliente con escenarios de prueba. Los escenarios de prueba de Apidog encadenan solicitudes con aserciones y comprobaciones de tiempo. Construye un escenario que se active contra tu mock inestable y afirme que la llamada finalmente tiene éxito, que el tiempo total transcurrido cae dentro de tu envoltura de retroceso esperada y que se creó exactamente un recurso (demostrando que tu clave de idempotencia hizo su trabajo). Conecta el escenario a CI y tu lógica de reintentos se ejercitará en cada commit en lugar de en cada interrupción.

Esta es la diferencia entre "añadimos reintentos" y "verificamos que nuestro cliente sobrevive a una dependencia con límite de tasa y parcialmente caída". Descarga Apidog gratis y podrás tener un servidor mock fallido ejecutándose contra tu cliente en unos diez minutos.

Preguntas Frecuentes

¿Debo reintentar un 429?

Sí, y es el único estado donde el servidor generalmente te dice cómo. Lee el encabezado Retry-After y espera al menos ese tiempo; recurre al retroceso exponencial con "jitter" si el encabezado falta. También trata los 429 repetidos como una señal para corregir tu tasa de solicitudes con limitación del lado del cliente o almacenamiento en caché, no como una operación normal.

¿Qué es el "full jitter"?

El "full jitter" elige cada retraso de reintento de forma uniforme y aleatoria entre cero y el techo exponencial: random(0, min(cap, base * 2^n)). Evita las oleadas de reintentos sincronizados de muchos clientes. En las simulaciones de AWS, superó al retroceso simple y al "equal jitter" tanto en el total de llamadas realizadas como en el tiempo hasta la finalización, por lo que es el valor predeterminado en los SDK de AWS.

¿Es seguro reintentar solicitudes POST?

Solo cuando la solicitud es idempotente en la práctica, lo que para POST significa enviar una clave de idempotencia que el servidor deduplica. Sin ella, un reintento después de un tiempo de espera puede duplicar un pago, un pedido o un registro, porque el servidor puede haber procesado la solicitud que crees que falló. Los agentes de IA que llaman a las API de escritura se encuentran con esto constantemente; los patrones de recuperación de errores del agente son los mismos que se cubren aquí: escrituras con clave, reintentos con límite y un disyuntor.

¿Cuántas veces debo reintentar?

De tres a cinco intentos manejan casi todas las fallas transitorias; más allá de eso, las tasas de éxito se estancan mientras la carga y la latencia siguen aumentando. Combina el límite por solicitud con un presupuesto global de reintentos (por ejemplo, los reintentos pueden agregar un 10% de tráfico adicional) para que una interrupción total no multiplique tu carga. Si una dependencia permanece caída después de tu último reintento, eso es territorio de disyuntores, no de reintentos.

Practica el diseño de API en Apidog

Descubre una forma más fácil de construir y usar APIs