En muchos backends con FastAPI, el envio de correo falla menos por SMTP que por trazabilidad. El primer intento sale, el segundo reintento también, y de pronto nadie sabe cuál mensaje corresponde a cuál evento. Cuando eso pasa en staging o en un entorno preview, el equipo pierde tiempo mirando bandejas mezcladas y logs medio utiles.
Hace un tiempo empecé a tratar estos flujos como una cadena completa: request, tarea en segundo plano, proveedor de correo y bandeja de validación. Si una pieza no conserva el mismo identificador, depurar se vuelve bastante incomodo. También aparecen notas raras en scripts viejos como tem email o temp org mail, que suelen indicar que el proceso fue creciendo sin una convención clara.
Dónde se rompen los reintentos de correo en FastAPI
El problema comun no es "el correo no llegó". El problema real es "llegaron dos correos parecidos y no sé cuál validó mi prueba". En FastAPI eso aparece mucho cuando:
- el endpoint genera el email y la cola usa otro
job_id - el worker reintenta pero no guarda el
trace_id - el test espera por asunto y no por contexto
- varios escenarios comparten la misma bandeja
Ese ultimo punto parece menor, pero crea ruido muy rapido. Si el asunto siempre dice algo como "Confirma tu acceso", cualquier retry viejo puede hacer pasar una prueba que en realidad deberia fallar.
Qué conviene trazar desde la API hasta la bandeja de prueba
Mi regla simple es esta: el mismo identificador debe vivir en la API, en la tarea y en el correo final. No hace falta exponer datos sensibles; basta con una referencia estable que puedas buscar en logs y contenido del mensaje.
Un payload pequeño ya ayuda bastante:
from pydantic import BaseModel
class EmailCommand(BaseModel):
trace_id: str
user_id: str
template: str
retry_count: int = 0
Cuando el endpoint crea la tarea, guarda trace_id en la base de datos o en la cola. Luego el worker lo vuelve a escribir en logs estructurados y también en un header o comentario visible del email. No es elegante a nivel literario, pero si te ahorra 20 minutos de investigación, ya pagó su costo.
Si ya estás ordenando pruebas de notificaciones, esta guía sobre probar emails transaccionales en FastAPI conecta muy bien con el mismo objetivo: aislar eventos para que cada mensaje tenga una lectura clara.
Un patrón corto con FastAPI y una cola simple
No siempre hace falta meter Celery, Kafka o una arquitectura enorme. Para muchos equipos basta con un patrón corto y honesto:
- El endpoint registra
trace_idyretry_count=0. - La tarea de envio captura errores transitorios.
- Si hay retry, incrementa
retry_countpero mantiene el mismotrace_id. - La prueba busca el correo por esa referencia, no solo por destinatario.
Una version reducida se ve así:
from fastapi import FastAPI, BackgroundTasks
from uuid import uuid4
app = FastAPI()
def send_email(trace_id: str, user_email: str, retry_count: int = 0) -> None:
# Aqui iria tu cliente SMTP o API externa.
print({"trace_id": trace_id, "user_email": user_email, "retry_count": retry_count})
@app.post("/signup")
def signup(user_email: str, background_tasks: BackgroundTasks):
trace_id = f"signup-{uuid4()}"
background_tasks.add_task(send_email, trace_id, user_email, 0)
return {"ok": True, "trace_id": trace_id}
Lo util de este enfoque no es el snippet. Lo util es que el test puede arrancar desde la respuesta HTTP y seguir el mismo trace_id hasta el inbox de prueba. Ahí dejas de perseguir fantasmas.
Otro aprendizaje: separa la bandeja por escenario o por suite. Si corres smoke tests de signup y recuperación sobre la misma casilla, tarde o temprano tendrás un falso positivo. Ese mismo criterio operativo también aparece cuando otros equipos necesitan probar correos de handoff en guardias SRE: menos mezcla, menos lectura ambigua, menos desgaste.
Checklist para no perseguir falsos positivos
Esta checklist me funciona bien en repos pequeños y medianos:
- Cada request crea un
trace_idque sobrevive al retry. - El worker registra
retry_counten logs y métricas. - El correo de prueba incluye una referencia visible del flujo.
- La suite valida contenido y correlación, no solo llegada.
- Cada escenario usa una bandeja aislada o un filtro propio.
- Los reintentos viejos se limpian despues de cada corrida.
No suena muy sofisticado, lo sé. Pero cuando una API empieza a enviar correos desde varios puntos del producto, este orden basico evita varios dolores de cabeza.
Preguntas frecuentes
¿Debo probar todos los reintentos en cada deploy?
No. Yo dejaría un smoke test para el camino feliz y una prueba dedicada para el retry cuando cambian colas, templates o timeouts. Si lo haces todo en cada push, la suite se vuelve lenta sin ganar mucha señal.
¿Qué suele romperse primero?
La correlación. El correo llega, pero el identificador no coincide con el evento que disparó el envio. En ese punto el test queda verde por accidente, que es peor que un fallo honesto.
¿Vale la pena guardar el trace_id dentro del email?
Sí, aunque sea en una parte discreta del cuerpo o en un header interno. Es una ayuda pequeñita, pero vuelve la depuración mucho más directa cuando algo sale raro.
Top comments (0)