Un reintento es una política de resiliencia, no una forma de ocultar excepciones. Repite una llamada solo cuando el fallo sea transitorio y la operación pueda ejecutarse otra vez sin duplicar efectos. Sin límites, timeout y telemetría, los reintentos prolongan incidentes.
Definir la política
python -m pip install tenacity
from tenacity import (
retry,
retry_if_exception_type,
stop_after_attempt,
wait_random_exponential,
)
class ServicioNoDisponible(Exception):
pass
@retry(
retry=retry_if_exception_type(ServicioNoDisponible),
stop=stop_after_attempt(4),
wait=wait_random_exponential(multiplier=0.5, max=8),
reraise=True,
)
def consultar() -> dict[str, object]:
return llamar_servicio(timeout=3)
La política limita intentos, selecciona una excepción transitoria y aplica backoff exponencial con jitter. reraise=True expone la última excepción al código que llama.
No repitas automáticamente un POST que crea un pago, pedido o usuario. La API necesita un mecanismo de idempotencia antes de que sea seguro. Consulta la guía de seguridad de APIs Python.
Combinar timeout, registros y pruebas
El cliente HTTP o el driver define el timeout de cada llamada. Tenacity controla la serie de intentos. Registra operación, intento y duración, pero nunca credenciales ni datos personales. La guía de logging en Python ofrece una base útil.
En pruebas, reemplaza la espera y simula dos fallos transitorios antes de un éxito. Después genera un error permanente y confirma que no se repite. No dependas de un servicio externo inestable.
La documentación oficial de Tenacity, consultada el 22 de julio de 2026, cubre límites, esperas, predicados, callbacks y código asíncrono. Una política correcta define qué se repite, cuántas veces, durante cuánto tiempo y cómo se informa el fallo final.
Clasificar fallos antes de reintentar
Son transitorios una interrupción breve de red, un reset, un límite de solicitudes o una indisponibilidad temporal. Credenciales inválidas, entrada malformada, acceso prohibido y un recurso definitivamente ausente son fallos permanentes. Repetir el segundo grupo desperdicia capacidad.
No selecciones Exception. Traduce errores en el adaptador: timeout y respuesta 503 pueden convertirse en ServicioNoDisponible, mientras autenticación conserva otra excepción no reintentable.
Un resultado también puede indicar indisponibilidad, pero usa predicados de resultado solo si el dominio lo garantiza. Una lista vacía suele ser un resultado válido.
Combinar intentos y presupuesto temporal
from tenacity import stop_after_attempt, stop_after_delay
parada = stop_after_attempt(5) | stop_after_delay(20)
El presupuesto total no sustituye el timeout individual. Si una petición puede bloquearse 60 segundos, la política de 20 segundos no ofrece un límite real. Configura timeouts de conexión y lectura en el cliente.
El backoff deja recuperarse al servicio. Una espera fija sirve para recursos locales controlados; la exponencial encaja con servicios compartidos. El jitter evita que una flota se sincronice. Limita la espera máxima.
Respeta Retry-After cuando exista, con un límite seguro y fallback aleatorio. Valida el encabezado antes de convertirlo en duración.
Idempotencia y efectos
Las lecturas suelen poder repetirse. Las escrituras necesitan un diseño explícito. Un timeout después del envío no demuestra que el servidor rechazó la operación; quizá se perdió la respuesta.
Usa la misma clave de idempotencia durante todos los intentos de una operación lógica y una clave nueva para otra operación. En base de datos, combina transacción e invariantes como una restricción única. No incluyas correo, cobro o eventos en el bloque repetido sin evitar duplicados.
Decora la función más pequeña posible. Si el análisis y la persistencia ocurren después de la llamada remota, reintenta solo esa llamada. Un error local no debe repetir una solicitud remota exitosa.
Observar intentos
import logging
from tenacity import before_sleep_log
logger = logging.getLogger(__name__)
@retry(
retry=retry_if_exception_type(ServicioNoDisponible),
stop=stop_after_attempt(4),
wait=wait_random_exponential(multiplier=0.5, max=8),
before_sleep=before_sleep_log(logger, logging.WARNING),
reraise=True,
)
def obtener() -> dict[str, object]:
return llamar_servicio(timeout=3)
Registra operación, intento, espera y categoría. Excluye tokens, cuerpos completos y datos personales. Mide reintentos, agotamientos y éxitos posteriores. Muchos éxitos posteriores también pueden señalar un servicio degradado.
Evita registrar la misma excepción final en cada capa. El componente informa intentos y la frontera de la aplicación informa el impacto final con un identificador.
Probar sin dormir
def test_exito_en_tercer_intento() -> None:
llamadas = 0
@retry(
stop=stop_after_attempt(3),
wait=lambda estado: 0,
retry=retry_if_exception_type(ServicioNoDisponible),
reraise=True,
)
def operacion() -> str:
nonlocal llamadas
llamadas += 1
if llamadas < 3:
raise ServicioNoDisponible
return "ok"
assert operacion() == "ok"
assert llamadas == 3
Prueba una excepción permanente con una sola llamada y el agotamiento con el error final. En escrituras, verifica que todos los intentos usen la misma clave. Controla la dependencia y la espera sin simular Tenacity.
Código asíncrono y cancelación
Tenacity admite corutinas. Usa un cliente HTTP asíncrono; I/O bloqueante dentro de async def sigue bloqueando el event loop. Permite que la cancelación se propague para respetar apagados y plazos. No uses un predicado amplio que capture señales de cancelación.
Lista operativa
Documenta excepciones elegibles, máximo de intentos, demora aproximada, timeout por llamada e idempotencia. Confirma que el plazo del llamador permite la política. Expón el agotamiento en logs y métricas y valida el mapeo real de errores en integración.
Los reintentos son una defensa estrecha ante fallos transitorios conocidos. No corrigen solicitudes inválidas, no reemplazan capacidad ni vuelven seguros los efectos duplicables. Clasificación y presupuestos explícitos evitan carga oculta.
Revisar la política en producción
Una política de reintentos es una hipótesis operativa. Tras publicarla, compara éxito inicial, éxito posterior, agotamiento y latencia total. Si casi todos los intentos fallan, el predicado puede incluir errores permanentes. Si funcionan con demasiada latencia, hay que revisar la dependencia o los timeouts.
Alerta sobre operaciones lógicas agotadas, no solo sobre intentos. Una caída multiplica llamadas, por lo que los paneles deben separar demanda original de tráfico repetido. Durante sobrecarga, circuit breaker, límites de concurrencia o rechazo controlado pueden proteger mejor.
Revisa la política si la API cambia estados, límites o idempotencia. Mantén el mapeo del adaptador cubierto por integración. Una lectura puede dejar de ser segura si el endpoint empieza a iniciar trabajo.
En procesos por lotes, decide si un elemento fallido detiene todo, pasa a reparación o se informa. No reintentes el lote completo, porque repetirías elementos ya terminados. Aplica la política a la unidad recuperable más pequeña.
Cuando el proceso termina por agotamiento, conserva suficiente contexto para una reparación manual segura: identificador de operación, último tipo de error y clave de idempotencia. Excluye datos sensibles.
Una revisión periódica debe comparar el presupuesto configurado con los plazos reales del llamador. Si ya no caben los intentos, reduce cantidad o espera en lugar de permitir que el trabajo continúe después de que el usuario haya abandonado.
Documenta además quién puede cambiar la política y cómo probarla. Un aumento de intentos no es un ajuste gratuito: multiplica tráfico durante una incidencia. Requiere revisión operacional, una prueba con fallos controlados y observación posterior del volumen.
Si la dependencia permanece degradada, ofrece un error claro al llamador y conserva el trabajo para recuperación cuando corresponda. No prolongues indefinidamente una petición interactiva. Un fallo visible y acotado permite actuar; una espera silenciosa consume recursos y oculta el estado real del sistema.