FastAPI: colas pequeñas para verificar emails
Cuando un usuario se registra, verificar su email parece una tarea corta: crear un token, enviar un mensaje y devolver 200 OK. En producción, sin embargo, el proveedor de correo puede tardar, fallar o responder dos veces. Si hacemos todo dentro de la petición HTTP, una lentitud externa se convierte en una mala experiencia y en tests intermitentes.
En este artículo propongo un patrón sencillo de Python y FastAPI: aceptar la intención rápido, guardar un trabajo pequeño y procesarlo fuera de la petición. No hace falta empezar con una plataforma enorme. Una cola mínima, estados claros y reintentos limitados suelen dar mas control.
El problema de verificar dentro de la petición
Un endpoint síncrono suele hacer esto:
@app.post("/signup")
async def signup(data: Signup):
user = await create_user(data)
await send_verification_email(user)
return {"status": "pending"}
El código es facil de leer, pero mezcla tres responsabilidades: crear la cuenta, programar el email y hablar con un servicio externo. Si send_verification_email tarda ocho segundos, el cliente espera ocho segundos. Si se corta la conexión justo después del envío, el usuario puede repetir la petición y recibir dos mensajes.
Para flujos que usan un disposable email generator durante QA, este detalle aparece muy pronto: la prueba no sabe si debe esperar la respuesta HTTP, la entrega o ambas cosas. Separar esos momentos hace el sistema mas observable.
Una cola mínima con FastAPI
Podemos guardar un trabajo con una clave idempotente y dejar que un worker lo procese:
class EmailJob(BaseModel):
user_id: int
address: str
attempts: int = 0
status: str = "queued"
@app.post("/signup", status_code=202)
async def signup(data: Signup, jobs: JobQueue):
user = await create_user(data)
await jobs.enqueue(
key=f"verify:{user.id}",
payload={"user_id": user.id, "address": user.email},
)
return {"user_id": user.id, "verification": "queued"}
El 202 Accepted comunica algo honesto: la petición fue aceptada, pero el email todavía no está enviado. En el worker, cambiaremos el estado a sending, sent o failed. Así el frontend puede mostrar un estado útil y el soporte puede revisar que pasó.
Estados e idempotencia
La clave del trabajo debe ser estable. Una nueva pulsación de “reenviar” no debería crear cinco tareas iguales. Guarda también last_sent_at, attempts y un provider_message_id cuando exista.
Un worker simple puede verificar el estado antes de enviar:
async def process(job: EmailJob):
current = await repo.get(job.user_id)
if current.verification_status == "sent":
return
await repo.mark_sending(job.user_id)
await mailer.send_verification(job.address)
await repo.mark_sent(job.user_id)
Si dos workers compiten, la transición a sending debe ser atómica en la base de datos. No confíes sólo en un if en Python; ese if no protege contra otra instancia del servicio.
Para ampliar la cobertura, conviene leer también estas pruebas limpias de emails transaccionales en FastAPI y revisar ideas de reenvíos verificados sin crear ansiedad operativa.
Reintentos con límites
No todos los errores merecen un reintento. Un timeout temporal puede volver a intentarse; una dirección rechazada permanentemente debe terminar en failed. Usa backoff, un máximo pequeño de intentos y una cola de revisión para los casos dudosos. Un retry infinito sólo es una fuga de recursos con otro nombre.
En logs, incluye job_id, user_id, intento y motivo, pero no el token completo. Eso permite reconstruir el incidente sin convertir los logs en una segunda bandeja de entrada. Si alguien busca “tamp mail com” o “tepm mail com” por error, esas variantes pueden aparecer en documentación de soporte, pero nunca deben decidir la lógica de entrega.
Checklist práctico
- Responder
202cuando el email es asíncrono. - Usar una clave idempotente por usuario y propósito.
- Persistir estados y transiciones atómicas.
- Separar errores temporales de rechazos permanentes.
- Limitar reintentos y medir la edad de la cola.
- Probar duplicados, timeouts y reinicios del worker.
- Mantener tokens fuera de logs y mensajes de error.
Una cola pequeña no resuelve cada problema, pero crea una frontera clara entre tu API y un proveedor externo. Empieza con estados que puedas explicar, reintentos que puedas pagar y métricas que alguien pueda leer el lunes por la mañana. Ese diseño suele ser suficiente para crecer sin hacer que el signup se sienta frágil.
Top comments (0)