sequenceDiagram participant U as Usuario participant API as API participant W as Trabajo en background U->>API: POST /auth/register API->>API: crea usuario (80ms) API-->>U: 201 — listo, sigue API->>W: send_welcome_email (3s, fuera del request) Note over W: si el proceso muere aquí,<br/>el email se pierde — por eso<br/>los trabajos importantes van a cola
30 21. Necesitamos responder ya y terminar el trabajo después
El email tarda 3 segundos; el usuario no debe esperarlos
El registro crea el usuario en 80ms… y luego manda el email de bienvenida que tarda 3s. La respuesta correcta no es “hacerlo más rápido”: es responder ya y terminar el trabajo después — con el límite honesto de que un trabajo en memoria muere si el proceso muere, y por eso existen las colas de verdad.
30.1 El problema
POST /auth/register hace tres cosas: guardar el usuario (80ms), enviar el email (3s), notificar a Slack (variable). El usuario solo necesita
Una variable es una caja con nombre donde guardas un valor: let total = 42 guarda el 42 bajo el nombre total. Puedes leerla y cambiarla después (total = 50). const = caja que no se puede reemplazar.
La autenticación responde “¿quién eres?” — login, contraseña, token. Se confunde con autorización (“¿qué puedes hacer?”), que es la pregunta siguiente. Primero te identificas, luego te dejan o no pasar.
una respuesta: “tu cuenta existe”. Los otros 3 segundos son trabajo que nadie está esperando — y sin embargo los está pagando en latencia, en timeouts del proxy, y en riesgo de que el email falle y parezca que el registro falló.
Un registry es el almacén de imágenes (Docker Hub, ECR, Artifact Registry): el CI empuja nexus:v42, el servidor la jala. Es el intermediario entre “se construyó” y “está corriendo”.
Un reverse proxy (nginx, ALB, Cloudflare) es el portero: recibe todo el tráfico en el 443, termina TLS y reparte a tu app interna. La app nunca mira a internet directamente — el proxy la protege.
30.2 Cómo lo resuelve un equipo
Un equipo serio separa lo que el usuario espera de lo que el sistema debe hacer: el response lleva solo lo primero; lo segundo va a un trabajo en segundo plano. La pregunta de diseño es una sola — “¿el usuario necesita esto para continuar?” No → background.
Y acepta el trade-off con los ojos abiertos: trabajo en memoria del proceso = si el proceso muere, el trabajo muere. Para “email que sería feo perder” se llega a colas de verdad (broker + worker + reintentos); para “estadística que se recalcula sola”, el background in-process alcanza y sobra.
Un proceso es un programa en ejecución: el sistema operativo le da memoria propia y un lugar en la fila del CPU. Tu API es un proceso; Postgres es otro; el navegador es varios. Que “se caiga el proceso” = el programa murió.
La memoria RAM es el escritorio de trabajo del programa: rápida, pero se borra al apagar. Los datos que deben sobrevivir van a disco o a la base de datos. Por eso let tasks = [] pierde todo al reiniciar.
30.3 Conceptos nuevos
- U Background work post-respuesta en cada stack
- TS
setImmediate/cola propia · PyBackgroundTasks· Gogo func()— y el mismo límite en los tres - U Colas de verdad (concepto): broker, worker, reintentos, idempotencia — BullMQ/RQ/asynq como panorama
- U El patrón
job_id+ polling: procesos largos sin timeout
30.4 La explicación visual
30.5 Implementación
Responder primero, trabajar después — el mismo patrón en los tres:
app.post("/auth/register", async (req, res, next) => {
try {
const user = await registerUser(req.body); // what the user waits for
res.status(201).json(toPublicUser(user)); // respond FIRST
setImmediate(() => { // work AFTER
sendWelcomeEmail(user.email).catch(err =>
logger.error({ err }, "welcome email failed")); // errors die here
});
} catch (err) { next(err); }
});@app.post("/auth/register", status_code=201)
def register(body: RegisterIn, bg: BackgroundTasks):
user = register_user(body) # what the user waits for
bg.add_task(send_welcome_email, user.email) # runs AFTER the response
return UserPublic.model_validate(user)
# FastAPI guarantees the task runs post-response — still in-processfunc register(w http.ResponseWriter, r *http.Request) {
user, err := registerUser(r.Context(), parseBody(r))
if err != nil { writeError(w, 422, errJSON); return }
writeJSON(w, 201, toPublic(user)) // respond first
go func(email string) { // work after
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := sendWelcomeEmail(ctx, email); err != nil {
slog.Error("welcome email failed", "err", err)
}
}(user.Email) // careful: detached ctx, not r.Context()
}En mi endpoint POST /auth/register [Express/FastAPI/net-http] el envío
del email de bienvenida hace que el response tarde 3s. Refactoriza para:
responder 201 apenas persista el usuario, ejecutar el email en
background post-respuesta con su propio contexto/timeout y manejo de
error que lo loguee sin tumbar nada, y documentar el límite (se pierde
si el proceso muere) más cómo se migraría a una cola BullMQ/RQ cuando el
volumen lo exija.
30.6 ¿Por qué cada stack lo hace así?
Lo único que necesitas llevarte: los tres separan responder de terminar — y los tres comparten el mismo límite: el trabajo vive en la memoria del proceso. Más allá de cierto volumen/criticidad, la respuesta correcta es una cola de verdad, no más go func().
Una función es una receta reutilizable: recibe ingredientes (parámetros), hace pasos y devuelve un plato (return). La escribes una vez y la llamas mil veces: add(2, 3) → 5.
| Nivel | Qué es | Cuándo basta | Qué pierdes |
|---|---|---|---|
| In-process | setImmediate / BackgroundTasks / go func() |
Volumen bajo, trabajo rehacible (emails, métricas) | Muere con el proceso; sin reintentos; sin visibilidad |
| Cola (BullMQ/RQ/asynq) | Job persistido en Redis/BD + worker separado | Trabajo que no debe perderse, reintentos, picos | Operar el broker y el worker |
| Managed (SQS, Cloud Tasks, Pub/Sub) | La nube opera la cola | Ya estás en la nube y quieres cero ops | Lock-in del proveedor |
Y el patrón para trabajos que duran minutos (exportar, reportes): POST /exports → 202 {"job_id": "j_9x"} → el cliente consulta GET /jobs/j_9x → {"status": "running"} → {"status": "done", "url": "..."}. Ningún request HTTP dura minutos — el trabajo sí, y tiene su propio recurso.
30.7 Errores comunes
| Error | Por qué pasa | Fix |
|---|---|---|
| Background que usa el contexto del request | “Funcionaba en dev” | El request muere al responder — ctx propio con su timeout |
| Error del job sin capturar | go func con panic → tumba el proceso |
El job captura su propio error y lo loguea |
| Creer que in-process = durable | “Se encoló, llegará” | Si importa, cola real — el proceso puede morir mañana |
| Encolar dentro de la transacción | El job corre antes del COMMIT y no ve la fila | Enqueue después del commit |
| Reintentar el email infinitamente | Sin contador de intentos | Max N intentos + dead-letter: los venenosos se apartan |
30.8 Buenas prácticas
- La pregunta antes que el código: “¿el usuario espera esto?” — si no, es job. Esa sola pregunta diseña el endpoint.
- 202 Accepted cuando el trabajo es la respuesta (exports, procesamiento): “aceptado, avísame” +
job_idpara consultar. - Idempotencia en el worker (cap. 22): los jobs se reintentan — “enviar email” debe poder correr dos veces sin mandar dos correos.
- Observa la cola: profundidad, duración, fallos — una cola que crece es un sistema muriendo en silencio (cap. 23).
30.9 Ejercicio
- Ordena estos trabajos del registro: ¿cuáles en el request y cuáles en background? — crear el usuario, enviar email de bienvenida, registrar “user_registered” en analytics, validar que el email no exista.
- ¿Por qué el
go funcde arriba usacontext.Background()y nor.Context()? - Diseña
POST /exportspara “descargar todas mis tareas en CSV” (toma 2 minutos): rutas, códigos, y qué guarda la tablajobs.
30.10 Mini reto
Diseñar el endpoint de “exportar mis datos” (toma minutos) sin timeout.
Ejercicio.
- Request: validar email único + crear usuario (lo que el usuario espera). Background: email de bienvenida + evento de analytics — ninguno bloquea la respuesta.
r.Context()se cancela cuando el response se envía — el job moriría al instante de nacer.context.Background()(más su propio timeout) le da vida propia.POST /exports→ crea filajobs(id, user_id, type, status)+ encola →202 {"job_id": "j_9x"}.GET /jobs/j_9x→{"status":"running"}luego{"status":"done","download_url":"..."}. El CSV va a object storage (cap. 22), no al response.
Mini reto. El patrón job_id + polling de arriba: ningún HTTP dura minutos, pero un job sí — y tiene su propio recurso que el cliente consulta. Bonus: con WebSockets (cap. 38) el servidor puede empujar “tu export está listo” en vez de que el cliente pregunte.
30.11 Vocabulario técnico del capítulo
Un objeto es una colección de datos con nombre: {name: "Ana", age: 30} — cada dato es una propiedad (clave → valor). Python los llama dict, Go los arma con struct, TS con objetos/interface.
Un bucle repite una acción por cada elemento o hasta cumplir una condición: for task in tasks hace algo con cada tarea. map y filter son bucles disfrazados de funciones — transforman/filtran colecciones sin for explícito.
Una clase es el molde de un objeto: define qué datos y qué métodos tiene. class Task es el molde; new Task() es una instancia concreta. Go no tiene clases — usa struct + métodos sueltos.
Un string es texto entre comillas: "hola". El nombre viene de “cadena de caracteres” — una secuencia de letras. Todo lo que llega de un formulario o una URL llega como string, aunque parezca número.
null (TS), None (Py), nil (Go) = “aquí no hay valor”. Es la respuesta a “¿qué devuelvo cuando no hay nada?” — y la fuente del bug más famoso de la historia (su inventor lo llamó “el error del billón de dólares”). Por eso el código revisa if x is not None.
return hace dos cosas a la vez: devuelve el resultado Y termina la función — lo que esté debajo nunca corre. return task = “aquí está el plato, salgo de la cocina”.
Asíncrono = empezar algo sin esperar sentado a que termine: pides la pizza (async) y sigues trabajando; cuando llega, te avisan. Lo opuesto a síncrono (esperar parado). Vital cuando la espera es larga: red, disco, bases de datos.
Un commit es una foto guardada del código con un mensaje que dice por qué cambió. La historia del proyecto es una cadena de commits: puedes volver a cualquiera, ver qué cambió y quién lo hizo.
Una base de datos es el programa que guarda datos de forma permanente y los responde rápido. Relacional (Postgres): tablas con relaciones. No relacional: documentos, clave-valor, grafos — cada una para una forma de dato distinta.
Una query es la pregunta que le haces a la base de datos en SQL: SELECT * FROM tasks WHERE done = false. La BD traduce la pregunta a un plan de búsqueda — por eso los índices importan.
Una transacción es un grupo de operaciones que se confirman juntas o no se confirma ninguna: transferir dinero = restar de A y sumar a B. Si falla a la mitad sin transacción, el dinero desapareció.
Un log es el diario del programa: qué pasó, cuándo, con qué request. Estructurado = en JSON con campos (request_id, user_id), para filtrar por máquina y no con los ojos.
Un caché es una copia rápida de algo costoso de obtener: el resultado de una query pesada, la sesión. Redis es el caché estándar. La regla de oro: cachear es fácil, invalidar (saber cuándo el caché ya no vale) es lo difícil.