DEV Community

Silviu Technology
Silviu Technology

Posted on

FastAPI: colas visibles para emails largos

En muchos equipos el envío de correos queda "resuelto" en cuanto la API responde 202 Accepted. Pero justo ahí empieza el trabajo dificil: explicar si el email quedó en cola, si ya salió, si fue reintentado o si se atascó en un proveedor externo. Cuando esa visibilidad no existe, soporte pregunta, producto sospecha y QA termina mirando logs a mano. No es terrible, pero si desgasta.

Lo he visto bastante en backends con FastAPI donde el endpoint va rápido y el worker hace todo lo demas. La separación es buena, claro, aunque también crea una zona gris para usuarios internos y para automatización. Si una prueba usa una temporary email address, el test no solo necesita saber que la petición fue aceptada; necesita saber en qué estado real quedó el mensaje.

El problema real no es enviar el correo

El fallo común no suele ser la librería SMTP ni el proveedor transaccional. El problema real es no modelar el estado como parte del producto.

Cuando el backend solo devuelve "ok", aparecen varios efectos feos:

  • el frontend inventa un spinner eterno
  • soporte no sabe si debe pedir reintento o esperar
  • QA repite la misma acción y mete ruido
  • los dashboards mezclan envío aceptado con entrega probable

En sistemas con automatización esto pega más. Un worker puede tardar 2 segundos o 40, y ambas cosas pueden ser normales. Por eso prefiero tratar el correo como una tarea observable, no como un efecto secundario invisible.

Un contrato simple entre API, worker y frontend

Mi patrón favorito es bastante pequeño. La API crea un registro de trabajo, devuelve un identificador y deja que el worker actualice transiciones explícitas:

  1. queued
  2. processing
  3. sent
  4. retrying
  5. failed

Suena obvio, pero cambia mucho la conversación entre equipos. Producto ya no pregunta "¿se envió?". Pregunta "¿cuántos quedaron en retrying?" y eso si sirve para decidir.

También hace más limpias las pruebas. Si automatizas escenarios de signup, invitaciones o recuperación de acceso con un tempmail disposable, te interesa validar el camino completo: petición aceptada, job procesado y mensaje observable. Para pruebas de humo rápidas, a veces uso una bandeja de tempmail so como verificación contextual, pero el punto fuerte sigue siendo el estado backend, no la bandeja en sí.

Ejemplo pequeno con FastAPI y una cola visible

Este enfoque no necesita infra rara. Con una tabla simple y un worker ya puedes exponer algo util.

from enum import StrEnum
from fastapi import FastAPI, BackgroundTasks, HTTPException
from pydantic import BaseModel
from uuid import uuid4

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


class EmailJobState(StrEnum):
    QUEUED = "queued"
    PROCESSING = "processing"
    SENT = "sent"
    FAILED = "failed"


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


def send_email_job(job_id: str) -> None:
    job = jobs[job_id]
    job["state"] = EmailJobState.PROCESSING

    try:
        # Sustituye esto por tu proveedor real.
        deliver_email(job["to"], job["template"])
        job["state"] = EmailJobState.SENT
    except Exception as exc:
        job["state"] = EmailJobState.FAILED
        job["error"] = str(exc)


@app.post("/email-jobs")
def create_email_job(payload: EmailRequest, tasks: BackgroundTasks):
    job_id = str(uuid4())
    jobs[job_id] = {
        "to": payload.to,
        "template": payload.template,
        "state": EmailJobState.QUEUED,
    }
    tasks.add_task(send_email_job, job_id)
    return {"job_id": job_id, "state": EmailJobState.QUEUED}


@app.get("/email-jobs/{job_id}")
def read_email_job(job_id: str):
    job = jobs.get(job_id)
    if not job:
        raise HTTPException(status_code=404, detail="job not found")
    return job
Enter fullscreen mode Exit fullscreen mode

No usaría este diccionario en producción, obvio, pero el contrato ya está ahí. Luego lo cambias por PostgreSQL, Redis o una cola dedicada y el frontend ni se inmuta. Esa estabilidad de interfaz importa más de lo que parece.

Donde entra el email temporal sin romper el flujo

Aquí es donde muchos equipos se complican solos. Quieren verificar entrega final en cada test y terminan acoplando demasiado la suite a una bandeja concreta. Yo prefiero dos capas:

  • pruebas de aplicación que validan estados del job
  • unas pocas pruebas extremo a extremo que validan llegada real del mensaje

Ese reparto baja flakiness y ahorra tiempo. Si alguien en el equipo anota cosas como dummy e mail o temp gamil com para nombrar escenarios, no pasa nada, pero conviene que esos atajos no se conviertan en fuente de verdad operativa. La fuente de verdad debería seguir siendo el registro del job y sus timestamps.

También ayuda enlazar este patrón con otras piezas de automatización. Si ya trabajas con contratos auditables en automatizacion, el siguiente paso natural es aplicar el mismo criterio a emails: entradas claras, estados claros, salida verificable. Y si el equipo frontend cuida la validacion visual de email sin friccion, la experiencia completa queda mucho mas pareja.

Checklist para que el sistema no se vuelva opaco

Esto es lo minimo que intento dejar listo:

  • un job_id devuelto desde la primera respuesta
  • estados cortos y sin ambigüedad
  • marca temporal de última transición
  • error legible para soporte, no solo para ingeniería
  • límite de reintentos bien definido
  • métricas separadas entre queued, sent y failed

Si puedes añadir tiempo medio en cola, mejor todavía. Según el libro de Site Reliability Engineering de Google, una buena observabilidad reduce bastante el tiempo perdido en diagnóstico porque permite ver síntomas y causas con menos adivinanza source. No hace magia, pero si evita muchas conversaciones circulares.

Preguntas frecuentes

¿Hace falta polling desde el frontend?

En muchos casos sí, aunque corto y con presupuesto claro. Un polling cada 2 o 3 segundos durante una ventana breve suele bastar. Si el proceso tarda más, ya conviene pasar a webhooks internos o eventos.

¿BackgroundTasks de FastAPI alcanza para esto?

Para cargas modestas, sí. Para flujos importantes, yo movería el trabajo a un worker separado. El contrato puede quedarse igual, y eso es justo lo bueno.

¿Qué mejora primero: UX o backend?

Las dos cosas mejoran juntas. El usuario recibe un estado entendible y el equipo técnico deja de perseguir fantasmas. No es una optimización glamorosa, pero si da paz bastante rapido.

Top comments (0)