Un endpoint de FastAPI que envía un correo suele empezar con una regla sencilla: si algo falla, reintenta. Funciona hasta que una dirección no existe, el proveedor responde con límite de frecuencia o la aplicación ya entregó el mensaje pero perdió la respuesta. En esos casos, repetir la misma llamada no arregla el problema; puede duplicar mensajes y esconder la causa.
Cuando automatizo flujos de backend, trato el envío como una operación con estados. Una dirección de correo desechable o una dirección normal puede servir para una prueba, pero el sistema debe saber si el error es temporal, permanente o desconocido. La palabra clave burner email address describe una necesidad de prueba frecuente, no una política de reintento.
El error no siempre significa que hay que reintentar
El primer paso es separar el fallo de transporte del resultado de negocio. Un timeout de red puede merecer un intento adicional. Un 400 porque falta un campo no lo merece. Un 202 Accepted tampoco confirma que el mensaje llegó: solo indica que el proveedor aceptó la petición.
Una clasificación pequeña suele ser suficiente:
- transitorio: timeout, conexión cerrada o respuesta
429; - permanente: dirección inválida, credenciales rechazadas o plantilla inexistente;
- ambiguo: se perdió la respuesta después de enviar la petición;
- de negocio: el usuario ya verificó su cuenta o la solicitud está duplicada.
Sin esta separación, los fallos no es más fáciles de resolver con un bucle infinito. El reintento debe ser una decisión, no un reflejo.
Clasificar antes de decidir
Podemos representar el resultado del proveedor con un modelo pequeño. No hace falta crear una jerarquía compleja de excepciones para cada código.
from enum import StrEnum from pydantic import BaseModel class DeliveryClass(StrEnum): TRANSIENT = "transient" PERMANENT = "permanent" AMBIGUOUS = "ambiguous" class DeliveryResult(BaseModel): classification: DeliveryClass provider_code: str | None = None message_id: str | None = None Después, la función que llama al proveedor convierte respuestas concretas en este contrato. El resto de la aplicación no necesita conocer cada peculiaridad del SDK. Esta frontera tambien facilita las pruebas: se pueden simular un 429, un timeout y una respuesta ambigua sin levantar un servidor de correo completo.
Un pequeño contrato de errores en FastAPI
El endpoint debe devolver una respuesta estable al cliente, aunque internamente el envío sea asíncrono. Por ejemplo, una petición de verificación puede devolver 202 con un identificador de operación. El cliente consulta el estado, y el worker decide si reintenta.
from fastapi import FastAPI, status from pydantic import BaseModel, EmailStr app = FastAPI() class VerificationRequest(BaseModel): email: EmailStr @app.post("/verification", status_code=status.HTTP_202_ACCEPTED) async def request_verification(payload: VerificationRequest) -> dict[str, str]: operation_id = await enqueue_verification(payload.email) return {"operation_id": operation_id, "state": "queued"} La cola debe conservar una clave idempotente, por ejemplo verification:{user_id}:{purpose}. Así, si el worker no sabe si la primera llamada terminó, puede consultar o repetir con la misma clave. Cada reintento tienen que conservar el mismo identificador de operación; generar uno nuevo crea dos historias que luego nadie puede unir.
Automatizar reintentos con límites
Un worker práctico usa tres límites: número de intentos, tiempo total y tipo de error. Un 429 puede esperar con backoff, mientras que una dirección inválida debe terminar en una cola de revisión o en un estado visible para el usuario.
async def deliver(job: VerificationJob) -> DeliveryResult: for attempt in range(3): result = await provider.send(job.message, idempotency_key=job.key) if result.classification != DeliveryClass.TRANSIENT: return result await asyncio.sleep(2**attempt) return DeliveryResult(classification=DeliveryClass.AMBIGUOUS) El backoff no es una garantía de entrega. Es solo una forma de no concentrar tráfico cuando el proveedor está lento. Si la respuesta sean ambiguo después del límite, guarda la operación para una reconciliación posterior; no la marques como entregada.
Observabilidad útil para el equipo
Los logs deben responder tres preguntas: qué operación falló, en qué estado quedó y quién puede actuar. Registra operation_id, clasificación, código del proveedor, número de intento y duración. Evita el cuerpo completo del mensaje y los tokens de verificación.
También conviene medir la proporción de cada clase. Un aumento de errores permanentes puede indicar datos malos en el formulario. Muchos ambiguos pueden apuntar a un timeout demasiado corto. Sus logs queda mucho más útil cuando distinguen esas dos situaciones.
En CI, la misma idea evita limpiar evidencia importante junto con datos sensibles. Puedes complementar este flujo con una guía para probar alertas después de un cambio de red y con prácticas para limpiar datos de email en CI sin perder señales. Si una búsqueda informal dice “fake e mail com”, documenta el término como entrada de prueba, pero no lo conviertas en una decisión técnica.
Checklist y preguntas frecuentes
Antes de publicar el endpoint, revisa:
- ¿Cada error tiene una clasificación explícita?
- ¿El reintento está limitado por tiempo y cantidad?
- ¿La operación tiene una clave idempotente?
- ¿Se puede reconciliar un resultado ambiguo?
- ¿Los logs excluyen tokens y cuerpos de correo?
- ¿Las pruebas cubren
429, timeout,400y duplicados?
¿Debo reintentar cualquier error 5xx? No siempre. Un 5xx puede ser temporal, pero si el proveedor ya aceptó la operación y solo falló la respuesta, repite únicamente con idempotencia o consulta el estado primero.
¿Es suficiente comprobar que existe un message_id? No. Es una señal de aceptación, no necesariamente de entrega. Conserva el estado del proveedor cuando esté disponible y define qué significa “entregado” para tu producto.
¿Qué hago con una dirección inválida? Marca el fallo como permanente, evita reintentos automáticos y muestra una acción clara. Reintentar datos que no van a cambiar solo consume cuota y ensucia las métricas.
La automatización gana calidad cuando cada fallo tiene un significado operativo. En FastAPI, un contrato pequeño, una clave idempotente y límites visibles suelen aportar más que otro bucle de reintento escondido en el cliente.
![Robotics Engineering Standards — HRS-013: Quadruped [EN]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F42u7msle9fmc8ba0f0p8.png)