tenacity
tenacity añade lógica de reintento configurable a cualquier función invocable: retroceso exponencial, jitter, condiciones de parada y hooks, sin tener que escribir manualmente bucles while alrededor de llamadas de red o base de datos inestables.
Receta
from tenacity import retry, stop_after_attempt, wait_exponential_jitter
@retry(stop=stop_after_attempt(3), wait=wait_exponential_jitter(initial=1, max=10))
def fetch_status() -> dict:
response = httpx.get("https://api.example.com/health", timeout=5.0)
response.raise_for_status()
return response.json()Cuándo usar esto:
- Errores transitorios HTTP 502/503/timeout de APIs upstream.
- Deadlocks de bases de datos o interrupciones de conexión.
- Limitación de velocidad de APIs en la nube (combinado con conocimiento de límites de tasa).
- Tareas de trabajadores que deberían sobrevivir a breves contratiempos de infraestructura.
Ejemplo de Trabajo
from __future__ import annotations
import logging
import httpx
from tenacity import (
before_sleep_log,
retry,
retry_if_exception_type,
stop_after_attempt,
wait_exponential_jitter,
)
logger = logging.getLogger(__name__)
class TransientError(Exception):
pass
def _raise_for_transient(response: httpx.Response) -> None:
if response.status_code >= 500:
raise TransientError(f"error del servidor {response.status_code}")
response.raise_for_status()
@retry(
retry=retry_if_exception_type((TransientError, httpx.TimeoutException)),
stop=stop_after_attempt(5),
wait=wait_exponential_jitter(initial=0.5, max=8),
before_sleep=before_sleep_log(logger, logging.WARNING),
reraise=True,
)
def charge_customer(customer_id: str, amount_cents: int) -> str:
with httpx.Client(timeout=10.0) as client:
response = client.post(
"/charges",
json={"customer_id": customer_id, "amount_cents": amount_cents},
headers={"Idempotency-Key": f"{customer_id}:{amount_cents}"},
)
_raise_for_transient(response)
return response.json()["id"]
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
print(charge_customer("cus_1", 1999))Lo que esto demuestra:
- Reintentos solo en
TransientErrory timeouts - no en errores de cliente 4xx. - Retroceso exponencial con jitter reduce las "manadas de relámpagos" (thundering herds).
before_sleep_logmuestra los intentos de reintento en los logs.- Encabezado de idempotencia emparejado con reintentos para seguridad de pagos.
reraise=Truepreserva la excepción final tras el agotamiento.
Inmersión Profunda
Cómo Funciona
- Decoradores -
@retryenvuelve funciones síncronas; usa@retryen funciones asíncronas con espera/parada compatibles con async (o el contextoAsyncRetrying). - Políticas de parada - Combina
stop_after_attempt,stop_after_delay,stop_any. - Políticas de espera - Fija, exponencial, aleatoria o una función llamada personalizada.
- Predicados de reintento -
retry_if_exception,retry_if_result,retry_unless_exception_type.
Matriz de Decisión de Reintento
| Tipo de Error | ¿Reintentar? | Notas |
|---|---|---|
| HTTP 408/429/502/503/504 | A menudo | Respeta Retry-After cuando está presente |
| HTTP 400/401/404 | No | Corrige la solicitud o la autenticación |
| Deadlock de DB | Sí | Retroceso corto, intentos limitados |
| ValidationError | No | Los datos no cambiarán al reintentar |
Notas de Python
from tenacity import Retrying
# Estilo imperativo dentro del cuerpo de un bucle
for attempt in Retrying(stop=stop_after_attempt(3)):
with attempt:
do_work()Errores Comunes
- Reintentar POST no idempotente - Doble cargo o registros duplicados. Solución: claves de idempotencia o tabla de deduplicación.
- Reintento infinito con configuración incorrecta - URL incorrecta reintenta para siempre con cron. Solución:
stop_after_attemptmás alerta ante el fallo final. - Capturar
Exceptiongenérico - Enmascara errores de programación. Solución: estrecharretry_if_exception_type. - Sin jitter - Clientes sincronizados reintentan juntos y realizan ataques DDoS al servicio que se recupera. Solución:
wait_exponential_jitter. - Reintento dentro de reintento - Decoradores anidados multiplican los intentos. Solución: un límite de reintento por llamada saliente.
- Fallos silenciosos - Ignorar la última excepción oculta las interrupciones. Solución:
reraise=Truey métricas sobre el recuento de reintentos.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
Bucle for manual | Scripts de un solo uso con 2 reintentos | Políticas compartidas entre servicios |
urllib3.util.retry | Solo en el stack requests/urllib3 | Aplicaciones nativas de httpx |
autoretry_for de Celery | Reintentos de cola de tareas a nivel de trabajador | Llamadas de cliente HTTP en proceso |
| Disyuntor (Circuit breaker) (pybreaker) | Fallo sostenido del upstream | Solo breves interrupciones transitorias |
Preguntas Frecuentes
¿Funciona tenacity con funciones asíncronas?
Sí - usa helpers de reintento async o AsyncRetrying dependiendo de la versión de tenacity; mantén un bucle de eventos por cliente.
¿Cómo reintento en un estado HTTP sin excepciones personalizadas?
Lanza una TransientError de dominio después de inspeccionar response.status_code dentro de la función envuelta.
¿Puedo notificar a Slack ante un fallo final?
Usa retry_error_callback o envuelve la llamada y maneja la excepción relanzada en el llamador.
¿Cuántos intentos son suficientes?
Empieza con 3-5 para HTTP; ajusta según la latencia p99 y el SLO del upstream - registra métricas de reintento para decidir.
¿Deben reintentar los trabajadores o el cliente?
Ambas capas son válidas - los reintentos del cliente manejan interrupciones de subsegundo; los reintentos de Celery manejan reinicios de procesos.
¿Cómo pruebo la lógica de reintento?
Simula la dependencia inestable para que falle dos veces y luego tenga éxito; verifica que el recuento de llamadas sea tres.
¿tenacity respeta las cabeceras Retry-After?
No automáticamente - analiza la cabecera en before_sleep y devuelve una espera personalizada si es necesario.
¿Puedo deshabilitar los reintentos en las pruebas?
Pasa una política de reintento "no-op" mediante inyección de dependencias o establece una bandera de entorno que devuelva la función desnuda.
¿Qué pasa con el rollback de la sesión de base de datos?
Reintenta en el límite de la unidad de trabajo después de session.rollback() - no reintentes a mitad de transacción.
¿Es tenacity seguro para hilos (thread-safe)?
El decorador es reentrante por llamada; no compartas estado mutable dentro de las funciones reintentadas sin bloqueos.
Relacionado
- httpx y requests - Clientes HTTP para envolver
- Reintentos y Retroceso (Backoff) - patrones a nivel de aplicación
- celery - reintentos de tareas distribuidas
- Limitación de Tasa (Rate Limiting) - cuándo no reintentar
Versiones de Stack: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.