DEV Community

Silviu Technology
Silviu Technology

Posted on

FastAPI: espera util para correos async

Cuando una API lanza un correo en background, el error no suele estar en send_email() sino en lo que pasa entre el request y la verificacion final. En equipos Python lo vi muchas veces: el endpoint responde 202 Accepted, la tarea async corre en otro proceso y, unos minutos despues, nadie sabe si el correo salio tarde, si fallo o si el usuario hizo tres reintentos por ansiedad. Ese hueco de espera se siente pequeño, pero pega fuerte en soporte y en debugging.

En FastAPI prefiero tratar esa espera como parte del contrato, no como un detalle que "ya veremos". Si el backend deja una pista util desde el primer segundo, el flujo se vuelve mucho mas facil de operar. Tambien ayuda cuando QA prueba cuentas raras o seeds con texto feucho tipo temp gamil com y fake e mail com, porque la evidencia no queda mezclada con otras corridas.

Por que la espera sin contexto rompe el flujo

El problema normal es este:

  1. El cliente dispara una accion que envia correo.
  2. La API devuelve rapido para no bloquear.
  3. La tarea en background hace el trabajo real.
  4. El frontend o el tester no sabe bien que esperar ni donde mirar.

Parece una arquitectura correcta, y muchas veces lo es. Lo que falta es un punto intermedio: un estado consultable y corto de entender. Si no existe, aparecen tickets como "no llego el correo" cuando en realidad llego 40 segundos despues, o peor, se reintenta y terminas con duplicados. Segun el reporte State of DevOps 2024 de DORA, los equipos con mejores bucles de feedback y observabilidad entregan cambios con menos friccion operativa source. No es magia, es visibilidad.

Tambien conviene separar medicion de activacion y pruebas tecnicas. Si mezclas ambos usos en la misma inbox, las metricas quedan medio torcidas. Este enfoque conversa bien con la idea de medir activacion por email sin ensuciar metricas: una pequena decision de backend evita que producto lea ruido como si fuera uso real.

El patron simple en FastAPI

Mi patron favorito cabe en tres piezas:

  1. El endpoint crea un job_id.
  2. La tarea async persiste transiciones de estado.
  3. El cliente consulta ese estado o recibe un webhook interno.

Un ejemplo minimo:

from fastapi import BackgroundTasks, FastAPI
from pydantic import BaseModel
from uuid import uuid4

app = FastAPI()
JOB_STORE: dict[str, dict[str, str]] = {}


class EmailRequest(BaseModel):
    email: str
    template: str


def deliver_email(job_id: str, email: str, template: str) -> None:
    JOB_STORE[job_id] = {"status": "sending", "email": email}
    try:
        # Aqui iria tu proveedor real
        JOB_STORE[job_id] = {"status": "sent", "email": email, "template": template}
    except Exception:
        JOB_STORE[job_id] = {"status": "failed", "email": email}


@app.post("/emails")
def create_email_job(payload: EmailRequest, tasks: BackgroundTasks):
    job_id = str(uuid4())
    JOB_STORE[job_id] = {"status": "queued", "email": payload.email}
    tasks.add_task(deliver_email, job_id, payload.email, payload.template)
    return {"job_id": job_id, "status": "queued"}


@app.get("/emails/{job_id}")
def get_email_job(job_id: str):
    return JOB_STORE[job_id]
Enter fullscreen mode Exit fullscreen mode

No digo que un diccionario en memoria sea suficiente para produccion, obvio que no. Pero el modelo mental ya esta ahi: el request no promete "correo entregado", promete "trabajo aceptado con estado verificable". Esa diferencia baja mucha confusion, y aveces baja tambien el numero de reintentos manuales.

Que guardar para no depurar a ciegas

Si la tarea corre por fuera del request, yo guardaria al menos esto:

  1. job_id
  2. template
  3. recipient_hash o destinatario normalizado
  4. queued_at, sent_at o failed_at
  5. razon de fallo si existe

Con eso puedes responder casi todas las preguntas utiles sin abrir una novela de logs. Tambien sirve para construir runbooks claros. De hecho, la disciplina se parece bastante a estos runbooks de correo estables para LLMs: cuando cada paso deja un rastro corto y consistente, el sistema se vuelve menos misterioso y mas mantenible.

Un detalle que me gusta mucho es devolver un mensaje funcional al frontend, algo como: "Estamos procesando tu correo, revisa el estado en unos segundos". No es copy brillante, pero orienta. El usuario entiende que no necesita martillar el boton otra vez. Suena tonto, pero ahorra bastante ruido.

Donde entra un inbox temporal sin meter ruido

En pruebas de integracion, un correo temporal ayuda solo si esta bien encerrado por escenario. Si un mismo inbox recibe correos de signup, recovery y invites, la evidencia ya nace sucia. Yo prefiero un alias o inbox temporal por suite, con un tiempo de vida corto y tags por corrida. Para ese caso, herramientas como tempmailso sirven como apoyo operativo cuando quieres revisar entregas sin contaminar cuentas reales, pero igual lo importante sigue siendo la disciplina del flujo.

La regla simple seria esta:

  1. Un inbox por escenario sensible.
  2. Un job_id por request.
  3. Una consulta de estado antes de asumir fallo.
  4. Un timeout visible y razonable para QA.

Eso evita el clasico "no llego" dicho demasiado pronto. Tambien evita que el equipo confunda lentitud temporal con error permanente. No resuelve todo, pero ordena mucho.

Preguntas frecuentes

BackgroundTasks alcanza o mejor una cola real?

Para flujos chicos, BackgroundTasks alcanza bastante bien. Si necesitas retries, throughput alto o aislamiento fuerte, me iria antes a Celery, RQ o una cola manejada. Depende del trafico, claro.

Cuanto deberia esperar el frontend?

Yo suelo mostrar estado inmediato y luego consultar durante 15 a 45 segundos, segun el proveedor de correo y el tipo de mensaje. Si no llega confirmacion, mejor enseñar "sigue en proceso" que marcar error demasiado pronto.

Esto tambien mejora SEO o solo operacion?

Mejora sobre todo operacion y experiencia real. Pero cuando el producto tiene menos tickets y menos friccion en onboarding, el impacto secundario en conversion existe. No es directo, pero se nota.

Top comments (0)