Cuando un SaaS empieza a tener usuarios, los emails transaccionales aparecen por todas partes: bienvenida, verificación, recuperación de contraseña, avisos de pago y mensajes de marketing. Al principio cada endpoint puede enviar el suyo directamente. Parece práctico, pero luego nadie sabe que ocurrió cuando una persona dice: “no me llegó el correo”.
En mis experimentos de producto, una cola pequeña de emails ha sido una mejora muy rentable. No hace falta empezar con una plataforma enorme. Lo importante es conservar el contexto del signup, separar el envío de la petición web y hacer que cada reintento sea entendible. Incluso cuando probamos un generador de correo falso en QA, el equipo necesita saber qué trabajo produjo cada mensaje.
El problema de enviar sin contexto
Un endpoint que envía directamente suele conocer solo una parte de la historia. Sabe el user_id, pero no siempre guarda el motivo del email, la versión de la plantilla o el intento actual. Si el proveedor responde lento, el usuario vuelve a pulsar el botón y puede terminar con dos mensajes.
El síntoma parece un fallo del proveedor, aunque la causa sea nuestra aplicación. También complica el soporte: buscar por dirección y hora no siempre alcanza. En algún ticket alguien puede escribir tamp mail com, pero esa pista no explica qué paso del flujo estaba ejecutándose.
La primera solución es crear un trabajo de email con identidad propia. Un trabajo no es solamente “manda este contenido”; es una pequeña historia que otro proceso puede continuar.
Una cola pequeña y explicable
El flujo básico puede ser así:
- La API valida la acción del usuario.
- Guarda un trabajo con estado
pending. - Devuelve una respuesta rápida al frontend.
- Un worker toma el trabajo y llama al proveedor.
- El trabajo pasa a
sentofailedcon una razón clara.
El cambio más importante es que la petición no queda bloqueada esperando al proveedor. Esto mejora la sensación de velocidad y la productividad del equipo, porque los fallos se investigan desde una cola visible, no desde una pantalla de timeout.
En el frontend también conviene reservar espacio para avisos de email. Así un cambio de estado no mueve todo el formulario y la persona entiende que el proceso sigue en marcha.
Qué guardar en cada trabajo
Un registro mínimo puede tener estos campos:
id identificador único del trabajo
user_id usuario relacionado
kind verification, welcome o reset
template_version versión usada para renderizar
attempts número de intentos realizados
status pending, processing, sent o failed
last_error razón breve y segura del último fallo
created_at momento en que nació el trabajo
También guardaría un dedupe_key, por ejemplo verification:user-123:week-36. Con él podemos rechazar trabajos idénticos que todavía están pendientes. No guardaría el token completo de verificación en logs; el contexto debe ayudar a depurar sin convertir la cola en una fuga de datos.
Reintentos sin duplicar mensajes
Un reintento no significa repetir a ciegas. Antes de llamar al proveedor, el worker debe revisar si otro proceso ya marcó el trabajo como sent. Para evitar dos workers simultáneos, se puede usar un cambio atómico de estado o un lock con expiración corta.
Una política inicial y fácil de explicar podría ser:
- reintentar errores de red y respuestas temporales;
- no reintentar una dirección inválida;
- esperar cada vez un poco más, con un límite total;
- registrar el motivo y el número de intento;
- enviar el caso agotado a una cola de revisión.
La misma disciplina ayuda cuando hacemos correos de rollback sin confusión: cada alerta necesita decir qué ocurrió, qué se intentó y qué acción sigue.
Errores comunes
El primero es poner toda la lógica en el worker y dejar que la API cree trabajos duplicados. El segundo es usar failed para cualquier problema, sin distinguir un error permanente de uno temporal. El tercero es borrar trabajos antiguos demasiado pronto; luego el equipo pierde la evidencia necesaria para entender una incidencia.
Otro error es medir únicamente “emails enviados”. Yo añadiría tiempo en cola, tasa de reintento, duplicados evitados y trabajos sin contexto. Esas métricas cuentan mejor la salud del flujo.
Checklist de implementación
- [ ] Cada tipo de email tiene un
kindreconocible. - [ ] Existe una clave de deduplicación estable.
- [ ] El worker toma un trabajo de forma atómica.
- [ ] Los errores temporales y permanentes tienen rutas distintas.
- [ ] Los logs incluyen el
job_id, nunca secretos completos. - [ ] El frontend muestra un estado claro después del signup.
- [ ] Hay una vista para revisar trabajos agotados.
Recapitulación
Una cola de email no tiene que ser una gran plataforma desde el primer día. Empieza con identidad, estados y contexto. Separa la petición del envío, limita los reintentos y conserva una explicación pequeña de cada decisión.
Con ese patrón, añadir un correo de bienvenida o un flujo de recuperación deja de ser una aventura aislada. El SaaS crece con más orden, el backend es más fácil de observar y soporte recibe respuestas concretas cuando algo no sale bien.
Top comments (0)