Un job en segundo plano es una tarea que corre fuera del ciclo de la solicitud: la encola la app, la ejecutan workers, se reintenta si falla. La línea divisoria es el tiempo: una respuesta debe sentirse instantánea (unos cientos de milisegundos) y tiene que ganarle al timeout del gateway (típicamente ~30 segundos), mientras que el trabajo de verdad — correos por SMTP, procesamiento de imágenes, generación de reportes, APIs de terceros — toma de segundos a minutos y merece reintentos. Todo lo que queda del lado equivocado de esa línea se encola, se confirma y se hace en otra parte; a escala, esto es infraestructura central, no plomería — una gran empresa de mensajería procesa más de 1.000 millones de jobs al día con exactamente esta maquinaria.
Puntos clave
| Pregunta | Respuesta |
|---|---|
| La arquitectura | El productor encola → la cola persiste → el worker ejecuta y envía el ack |
| La línea divisoria | Presupuesto de respuesta ~300 ms; lo lento o reintentable se vuelve job |
| Las tres programaciones | Inmediata · diferida (“en 24 h”) · recurrente (cron) |
| El contrato de confiabilidad | Al-menos-una-vez + jobs idempotentes + backoff con jitter + dead letters |
| La falla silenciosa | Nada da error cuando un job programado no corre — monitorea la ausencia |
El antipatrón que lo enseña todo
// ✗ El registro que se vuelve rehén de un servidor de correo
app.post('/signup', async (req, res) => {
const user = await createUser(req.body);
await sendWelcomeEmail(user); // ¿SMTP lento? El usuario mira un spinner.
res.json(user); // ¿SMTP caído? El registro da 500 — pero
}); // la cuenta EXISTE. Lo peor de ambos mundos.
// ✔ Encola y responde
app.post('/signup', async (req, res) => {
const user = await createUser(req.body);
await jobs.enqueue('welcomeEmail', { userId: user.id }); // milisegundos
res.json(user); // el correo se envía, reintenta y llega
}); // a su propio ritmo — invisiblemente
El mismo principio visto desde ambos lados de un backend real — un job sobre los datos, y un cliente que encola en lugar de esperar:
// JavaScript — Cloud Code (cloud/main.js)
// A background job: heavy work outside the request cycle
Parse.Cloud.job('sendWeeklyDigest', async (request) => {
const query = new Parse.Query(Parse.User).equalTo('digestOptIn', true);
await query.eachBatch(async (users) => {
for (const user of users) await sendDigest(user); // per-record, resumable
}, { useMasterKey: true });
request.message('Digest run complete'); // status visible in the dashboard
});
// Run on demand or on a schedule from the dashboard — no queue to operate // Flutter / Dart — Back4app Flutter SDK
// The client's rule: never wait on heavy work — enqueue and move on
final export = ParseObject('ExportRequest')
..set('user', currentUser)
..set('status', 'queued'); // an afterSave trigger starts the work
await export.save(); // returns in milliseconds
// Watch the job's progress like any other data:
final sub = await LiveQuery().client.subscribe(
QueryBuilder<ParseObject>(ParseObject('ExportRequest'))
..whereEqualTo('objectId', export.objectId));
sub.on(LiveQueryEvent.update, (job) {
if (job.get<String>('status') == 'done') openReport(job.get('fileUrl'));
}); // iOS / Swift — Back4app Swift SDK
// The client's rule: never wait on heavy work — enqueue and move on
var export = ExportRequest()
export.status = "queued" // an afterSave trigger starts the work
let saved = try await export.save() // returns in milliseconds
// Watch the job's progress like any other data:
let sub = try await ExportRequest.query("objectId" == saved.id).subscribe()
sub.handleEvent { _, event in
if case .updated(let job) = event, job.status == "done" {
openReport(job.fileUrl)
}
} // Android / Kotlin — Back4app Android SDK
// The client's rule: never wait on heavy work — enqueue and move on
val export = ParseObject("ExportRequest")
export.put("user", ParseUser.getCurrentUser())
export.put("status", "queued") // an afterSave trigger starts the work
export.save() // returns in milliseconds
// Watch the job's progress like any other data:
val q = ParseQuery.getQuery<ParseObject>("ExportRequest")
q.whereEqualTo("objectId", export.objectId)
val sub = ParseLiveQueryClient.Factory.getClient().subscribe(q)
sub.handleEvent(SubscriptionHandling.Event.UPDATE) { _, job ->
if (job.getString("status") == "done") openReport(job.getString("fileUrl"))
} Productor, cola, worker
Tres roles, un contrato. El productor — normalmente un handler de solicitudes — crea un job: un nombre de tipo y un payload pequeño. La cola lo persiste; la durabilidad es el punto, ya que el trabajo que existe solo en la memoria de un proceso agonizante muere con él. Los workers toman, ejecutan y confirman con un ack; un job solo desaparece una vez confirmado, y así es como el job de un worker caído sobrevive para correr de nuevo. El vocabulario que viene incluido: enqueue/dequeue, acks, visibility timeouts, y la distinción que vale la pena vigilar — una cola de mensajes mueve datos entre servicios (y puede repartirse a muchos suscriptores); una cola de jobs ejecuta trabajo, exactamente una vez por job por worker, con reintentos y estado incluidos.
Los tres modos de programación — y un repaso de cron
Todo sistema de jobs implementa los mismos tres modos de tiempo: inmediato (encola ahora, corre en cuanto un worker se libere), diferido (encola ahora, elegible en un momento futuro — recordatorios, expiración de trials, y el propio mecanismo de reintentos) y recurrente (un programador reencola por calendario). Las programaciones recurrentes casi siempre se escriben en los cinco campos de cron:
┌ minuto (0-59) ┌ hora (0-23) ┌ día del mes ┌ mes ┌ día de la semana
0 2 * * * → todos los días a las 02:00
*/15 * * * * → cada 15 minutos
0 9 * * 1 → los lunes a las 09:00
Dos minas del programador: las transiciones de horario de verano (las 02:30
desaparecen u ocurren dos veces — programa en UTC) y la superposición (una
ejecución que dura más que su intervalo necesita un lock, o dos copias
procesan los mismos datos).
Colas de jobs vs. colas de mensajes vs. programadores
| Cola de jobs | Cola de mensajes | Programador | |
|---|---|---|---|
| Unidad | Un job para ejecutar | Un mensaje para entregar | Una hora para disparar |
| Consumidores | Exactamente un worker | Uno o muchos suscriptores | Los jobs que él encola |
| Trae incluido | Reintentos, estado, prioridades, retraso | Durabilidad, enrutamiento, fan-out | Calendarios, recurrencia |
| Nombres open-source | Sidekiq, Celery, BullMQ | RabbitMQ, Kafka, Redis | cron, Quartz |
| Se compone como | Ejecuta lo que los eventos exigen | Transporta entre sistemas | Alimenta la cola a tiempo |
La composición es la respuesta a la mayoría de los debates de “¿cuál de ellos?”: los programadores deciden cuándo, las colas guardan qué, los workers hacen el hacer — y un lote nocturno es cron encolando jobs que los workers ejecutan con semántica completa de reintentos.
El contrato de confiabilidad
Las piezas suelen enseñarse por separado; son un solo contrato. La cola promete al-menos-una-vez — un worker que se cae después del trabajo pero antes del ack significa que el job corre de nuevo; esto es inevitable, no descuido. Tú prometes idempotencia a cambio: deduplica por un ID de job, protege con constraints únicos, escribe con upserts, para que la segunda ejecución sea un no-op en lugar de una segunda factura. Los reintentos con backoff exponencial y jitter atraviesan las fallas transitorias — 1 s, 2 s, 4 s, con aleatoriedad para que mil jobs que fallan juntos no reintenten juntos, una cortesía que los terceros con rate limiting impondrán si tú no la extiendes. La cola dead-letter atrapa el resto: tras el tope de intentos, los jobs se estacionan donde humanos pueden inspeccionarlos, corregirlos y reejecutarlos — y los jobs permanentemente malformados deberían saltarse el teatro de los reintentos e ir directo ahí.
Diseñar bien los jobs
Las reglas en las que los frameworks de colas coinciden, reunidas: pasa IDs, no objetos — un payload de job con userId: "u-8fk2" vuelve a buscar estado fresco al momento de correr, mientras que un objeto de usuario serializado ya está viejo en cuanto se encoló; mantén los payloads pequeños y JSON-simples, nunca secretos; un job por registro — un job por usuario que falla reintenta a un usuario, un mega-job reintenta a todos; haz el trabajo largo reanudable — pártelo en bloques, registra checkpoints de progreso y sal con gracia ante señales de apagado a mitad de bloque; y asume concurrencia — dos workers algún día procesarán jobs adyacentes que tocan la misma fila, una pregunta de locking que respondes en el diseño o en un incidente.
Vigilar la cola
La falla en segundo plano es silenciosa — ningún usuario ve una página de error cuando muere el job del resumen. Los cuatro medidores que reemplazan la página de error: profundidad de la cola (un backlog que se acumula significa que los workers van perdiendo); edad del job medida de encolado a finalización — un job procesado en 200 ms tras esperar 40 minutos es un job de 40 minutos para el usuario; tasas de falla y de dead-letter con alertas sobre la cola dead-letter, porque los jobs estacionados que alguien olvidó son datos silenciosamente sin procesar; y ejecuciones perdidas del trabajo programado — la más traicionera de todas, ya que una programación que nunca se disparó no produjo error, línea de log ni job alguno. Alarma sobre la ausencia.
Casos de uso comunes
- Correo y notificaciones — el aplazamiento canónico: encola en el registro, entrega con reintentos.
- Procesamiento de medios — resizes, transcodificaciones, thumbnails: minutos de CPU que ninguna solicitud debería esperar.
- Reportes y exportaciones — genera en segundo plano, notifica cuando el archivo esté listo.
- Sincronización con terceros — syncs de CRM y entregas de webhook, con backoff contra remotos inestables.
- Limpieza y mantenimiento — barridos de TTL, poda de huérfanos, armado de resúmenes en el cron nocturno.
¿Qué patrón necesitas? Matriz de decisión
| El trabajo se ve como | Usa |
|---|---|
| Disparado por acciones de usuario, volumen variable | Cola de jobs + workers |
| Calendario fijo, acotado, predecible | Job programado (cron) |
| Lote nocturno sobre muchos registros | Cron encola; los workers ejecutan por registro |
| Servicios contándose cosas entre sí | Cola de mensajes / pub-sub |
| Trabajo lento dentro de un handler hoy | El refactor de arriba — encola y responde |
| Debe sobrevivir crashes y reintentar con seguridad | Cualquiera de estos — más el contrato de confiabilidad |
Limitaciones y trade-offs
- Eventual, no instantáneo. El trabajo encolado ocurre después — las UIs necesitan estados pendientes, y el “después” necesita un límite que alguien eligió a propósito.
- El estado se mueve fuera de banda. Los resultados llegan por campos de estado, callbacks o notificaciones — la simplicidad solicitud-respuesta ya se gastó; presupuesta la plomería.
- La infraestructura es real. Brokers, workers y programadores son servicios con sus propios modos de falla — o el problema de una plataforma, que es el argumento de los jobs gestionados.
- Los duplicados están garantizados, eventualmente. Al-menos-una-vez es el contrato; todo job no idempotente es un incidente con cuenta regresiva.
- Las colas esconden la sobrecarga con gracia — demasiada gracia. Un backlog que crece se ve tranquilo justo hasta que la edad de los jobs explota; las alarmas de profundidad y edad son el mecanismo de honestidad.
Jobs en Segundo Plano en Back4app
Back4app es una plataforma open-source de Backend as a Service (BaaS) que combina base de datos gestionada, APIs REST y GraphQL generadas automáticamente, autenticación, almacenamiento de archivos y funciones serverless con Cloud Code. Los jobs aquí son Cloud Code con una programación: define Parse.Cloud.job — las pestañas de código muestran un job de resumen recorriendo usuarios con eachBatch, por registro y reanudable — y córrelo bajo demanda o con recurrencia estilo cron desde el dashboard, con estado y logs en el panel de Jobs; sin broker que aprovisionar, sin flota de workers que escalar, sin servicio de colas que mantener vivo. La mitad orientada a eventos se compone de los mismos primitivos: un handler de solicitudes escribe una fila con status: "queued", un trigger afterSave inicia el trabajo, y los clientes siguen el progreso por Live Queries en lugar de hacer polling — el patrón productor/worker expresado como datos, con el contrato de confiabilidad (IDs en los payloads, handlers idempotentes, estado sobre el que puedes alarmar) como tu disciplina de diseño en lugar de tu infraestructura.
Preguntas frecuentes
¿Qué es un job en segundo plano?
Una tarea que tu aplicación corre fuera del ciclo de solicitud y respuesta: el servidor encola el trabajo, le responde al usuario de inmediato, y un worker separado lo ejecuta de forma asíncrona — con reintentos si falla. Todo lo que sea lento, reintentable o innecesario para la respuesta pertenece ahí.
¿Por qué no hacer el trabajo directamente en el handler de la solicitud?
Porque el trabajo lento bloquea la respuesta, ata capacidad del servidor y choca con los timeouts del gateway — y si el trabajo falla a mitad de la solicitud, el usuario recibe un error aunque parte de él ya ocurrió. El ejemplo canónico: enviar el correo de bienvenida durante el registro, donde un servidor de correo lento convierte la creación de la cuenta en un spinner.
¿Cómo funciona una cola de jobs?
Es una lista durable entre productores y workers: la app empuja un job — un tipo más un payload pequeño —, la cola lo persiste, y los workers lo toman, lo ejecutan y confirman con un ack. Los jobs sin ack vuelven a la cola, y de ahí viene la confiabilidad.
¿Cuál es la diferencia entre una cola de jobs y una cola de mensajes?
El propósito. Una cola de mensajes mueve datos entre servicios — la entrega es la meta, y un mensaje puede repartirse a muchos consumidores. Una cola de jobs ejecuta trabajo — agrega reintentos, programación, prioridades y estado por encima, y cada job va a exactamente un worker.
¿Cuál es la diferencia entre cron jobs y jobs en segundo plano?
El disparador. Cron se guía por tiempo — "todas las noches a las 2 a.m."; los jobs en segundo plano se guían por eventos — "cuando un usuario sube un archivo". Los patrones se componen: un diseño común tiene a cron encolando el lote nocturno mientras los workers lo ejecutan con semántica completa de reintentos.
¿Cómo funcionan los reintentos de jobs?
Los jobs fallidos se vuelven a correr automáticamente con backoff exponencial — un segundo, luego dos, cuatro, ocho — más un jitter aleatorio para que mil fallas no reintenten todas en el mismo instante, con un tope máximo de intentos. El backoff atraviesa las fallas transitorias; el tope evita que las permanentes reintenten para siempre.
¿Por qué los jobs en segundo plano deben ser idempotentes?
Porque las colas prometen ejecución al-menos-una-vez: un worker puede caerse después de hacer el trabajo pero antes de confirmarlo con el ack, y el job corre de nuevo. Correr dos veces no puede cobrar dos veces — claves de idempotencia, constraints únicos y upserts convierten los duplicados en no-ops.
¿Qué es una cola dead-letter?
Adonde van los jobs tras agotar sus reintentos — retenidos para inspección, alertas y reejecución manual, en lugar de reintentar para siempre o desaparecer en silencio. Los jobs malformados que nunca van a tener éxito deberían saltarse los reintentos e ir directo ahí.