Um job em segundo plano é uma tarefa que roda fora do ciclo de requisição — enfileirada pelo app, executada por workers, com retry em caso de falha. A linha divisória é o tempo: uma resposta deve parecer instantânea (poucas centenas de milissegundos) e precisa vencer o timeout do gateway (tipicamente ~30 segundos), enquanto trabalho de verdade — e-mails via SMTP, processamento de imagens, geração de relatórios, APIs de terceiros — leva de segundos a minutos e merece retries. Tudo do lado errado dessa linha é enfileirado, confirmado e feito em outro lugar; em escala, isso é infraestrutura central, não encanamento — uma grande empresa de mensagens processa mais de um bilhão de jobs por dia com exatamente essa maquinaria.
Principais pontos
| Pergunta | Resposta |
|---|---|
| A arquitetura | Produtor enfileira → fila persiste → worker executa e dá ack |
| A linha divisória | Orçamento de resposta ~300 ms; o que é lento ou repetível vira job |
| Os três agendamentos | Imediato · atrasado (“em 24 h”) · recorrente (cron) |
| O contrato de confiabilidade | Ao-menos-uma-vez + jobs idempotentes + backoff com jitter + dead letters |
| A falha silenciosa | Nada dá erro quando um job agendado não roda — monitore a ausência |
O anti-padrão que ensina tudo
// ✗ O cadastro que se faz refém de um servidor de e-mail
app.post('/signup', async (req, res) => {
const user = await createUser(req.body);
await sendWelcomeEmail(user); // SMTP lento? O usuário encara um spinner.
res.json(user); // SMTP fora do ar? O cadastro dá 500 — mas
}); // a conta EXISTE. O pior dos mundos.
// ✔ Enfileire e responda
app.post('/signup', async (req, res) => {
const user = await createUser(req.body);
await jobs.enqueue('welcomeEmail', { userId: user.id }); // milissegundos
res.json(user); // o e-mail envia, tenta de novo e chega
}); // no seu próprio ritmo — invisivelmente
O mesmo princípio visto dos dois lados de um backend real — um job sobre os dados, e um cliente que enfileira em vez 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"))
} Produtor, fila, worker
Três papéis, um contrato. O produtor — em geral um handler de requisição — cria um job: um nome de tipo e um payload pequeno. A fila o persiste; durabilidade é o ponto, já que trabalho que existe apenas na memória de um processo agonizante morre com ele. Os workers puxam, executam e confirmam com ack; um job só desaparece depois do ack, e é assim que o job de um worker que travou sobrevive para rodar de novo. O vocabulário que vem junto: enqueue/dequeue, acks, visibility timeouts, e a distinção que vale policiar — uma fila de mensagens move dados entre serviços (e pode espalhar para muitos assinantes); uma fila de jobs executa trabalho, exatamente uma vez por job por worker, com retries e status embutidos.
Os três agendamentos — e um primer de cron
Todo sistema de jobs implementa os mesmos três modos de tempo: imediato (enfileire agora, rode assim que um worker liberar), atrasado (enfileire agora, elegível em um momento futuro — lembretes, expiração de trials, e o próprio mecanismo de retry) e recorrente (um agendador reenfileira por calendário). Agendamentos recorrentes quase sempre são escritos nos cinco campos do cron:
┌ minuto (0-59) ┌ hora (0-23) ┌ dia do mês ┌ mês ┌ dia da semana
0 2 * * * → todo dia às 02:00
*/15 * * * * → a cada 15 minutos
0 9 * * 1 → segundas-feiras às 09:00
Duas minas do agendador: transições de horário de verão (02:30 some ou
acontece duas vezes — agende em UTC) e sobreposição (uma execução que dura
mais que o intervalo precisa de um lock, ou duas cópias processam os
mesmos dados).
Filas de jobs vs. filas de mensagens vs. agendadores
| Fila de jobs | Fila de mensagens | Agendador | |
|---|---|---|---|
| Unidade | Um job para executar | Uma mensagem para entregar | Um horário para disparar |
| Consumidores | Exatamente um worker | Um ou muitos assinantes | Os jobs que ele enfileira |
| Vem com | Retries, status, prioridades, atraso | Durabilidade, roteamento, fan-out | Calendários, recorrência |
| Nomes open-source | Sidekiq, Celery, BullMQ | RabbitMQ, Kafka, Redis | cron, Quartz |
| Compõe como | Executa o que os eventos exigem | Transporta entre sistemas | Alimenta a fila na hora certa |
A composição é a resposta para a maioria dos debates de “qual deles?”: agendadores decidem quando, filas guardam o quê, workers fazem o fazer — e um lote noturno é o cron enfileirando jobs que os workers executam com semântica completa de retry.
O contrato de confiabilidade
As peças costumam ser ensinadas separadas; elas são um único contrato. A fila promete ao-menos-uma-vez — um worker que trava depois do trabalho mas antes do ack significa que o job roda de novo; isso é inevitável, não desleixo. Você promete idempotência em troca: deduplique por um ID de job, proteja com constraints únicas, escreva com upserts, para que a segunda execução seja um no-op em vez de uma segunda fatura. Retries com backoff exponencial e jitter atravessam falhas transitórias — 1 s, 2 s, 4 s, com aleatoriedade para que mil jobs falhando juntos não tentem de novo juntos, uma cortesia que terceiros com rate limit vão impor se você não a estender. A fila dead-letter pega o resto: depois do teto de tentativas, os jobs estacionam onde humanos podem inspecioná-los, corrigi-los e reexecutá-los — e jobs permanentemente malformados deveriam pular o teatro dos retries e ir direto para lá.
Projetando jobs bem feitos
As regras em que os frameworks de fila concordam, reunidas: passe IDs, não objetos — um payload de job com userId: "u-8fk2" rebusca estado fresco na hora de rodar, enquanto um objeto de usuário serializado já está velho no momento em que foi enfileirado; mantenha payloads pequenos e JSON-simples, nunca segredos; um job por registro — um job por usuário que falha refaz um usuário, um mega-job refaz todo mundo; torne trabalho longo retomável — divida em blocos, registre checkpoints de progresso e saia graciosamente em sinais de shutdown no meio de um bloco; e assuma concorrência — dois workers um dia vão processar jobs adjacentes tocando a mesma linha, uma questão de locking que você responde no design ou em um incidente.
De olho na fila
Falha em segundo plano é silenciosa — nenhum usuário vê uma página de erro quando o job do resumo morre. Os quatro medidores que substituem a página de erro: profundidade da fila (backlog acumulando significa que os workers estão perdendo); idade do job medida do enfileiramento à conclusão — um job processado em 200 ms depois de esperar 40 minutos é um job de 40 minutos para o usuário; taxas de falha e de dead-letter com alertas sobre a fila dead-letter, porque jobs estacionados que alguém esqueceu são dados quietamente não processados; e execuções perdidas do trabalho agendado — a mais traiçoeira delas, já que um agendamento que nunca disparou não produziu erro, linha de log ou job algum. Alarme sobre a ausência.
Casos de uso comuns
- E-mail e notificações — o adiamento canônico: enfileire no cadastro, entregue com retries.
- Processamento de mídia — resizes, transcodes, thumbnails: minutos de CPU que nenhuma requisição deveria esperar.
- Relatórios e exports — gere em segundo plano, notifique quando o arquivo estiver pronto.
- Sincronização com terceiros — syncs de CRM e entregas de webhook, com backoff contra remotos instáveis.
- Limpeza e manutenção — varreduras de TTL, poda de órfãos, montagem de resumos no cron noturno.
Qual padrão você precisa? Matriz de decisão
| O trabalho parece | Use |
|---|---|
| Disparado por ações de usuário, volume variável | Fila de jobs + workers |
| Calendário fixo, limitado, previsível | Job agendado (cron) |
| Lote noturno sobre muitos registros | O cron enfileira; workers executam por registro |
| Serviços contando coisas uns aos outros | Fila de mensagens / pub-sub |
| Trabalho lento dentro de um handler hoje | O refactor acima — enfileire e responda |
| Precisa sobreviver a crashes e repetir com segurança | Qualquer um destes — mais o contrato de confiabilidade |
Limitações e trade-offs
- Eventual, não instantâneo. Trabalho enfileirado acontece depois — UIs precisam de estados pendentes, e o “depois” precisa de um limite que alguém escolheu de propósito.
- O estado se move fora da banda. Resultados chegam por campos de status, callbacks ou notificações — a simplicidade requisição-resposta foi gasta; orce o encanamento.
- A infraestrutura é real. Brokers, workers e agendadores são serviços com seus próprios modos de falha — ou problema de uma plataforma, que é o argumento dos jobs gerenciados.
- Duplicatas são garantidas, eventualmente. Ao-menos-uma-vez é o contrato; todo job não idempotente é um incidente com hora marcada.
- Filas escondem sobrecarga com graça — graça demais. Um backlog crescendo parece calmo até a idade dos jobs explodir; alarmes de profundidade e idade são o mecanismo de honestidade.
Jobs em Segundo Plano no Back4app
O Back4app é uma plataforma open-source de Backend as a Service (BaaS) que combina banco de dados gerenciado, APIs REST e GraphQL geradas automaticamente, autenticação, armazenamento de arquivos e funções serverless com Cloud Code. Jobs aqui são Cloud Code com um agendamento: defina Parse.Cloud.job — as abas de código mostram um job de resumo iterando usuários com eachBatch, por registro e retomável — e rode sob demanda ou em recorrência estilo cron pelo dashboard, com status e logs no painel de Jobs; sem broker para provisionar, sem frota de workers para escalar, sem serviço de fila para manter vivo. A metade orientada a eventos se compõe dos mesmos primitivos: um handler de requisição grava uma linha com status: "queued", um trigger afterSave inicia o trabalho, e os clientes acompanham o progresso por Live Queries em vez de fazer polling — o padrão produtor/worker expresso como dados, com o contrato de confiabilidade (IDs nos payloads, handlers idempotentes, status sobre o qual dá para alarmar) como sua disciplina de design em vez de sua infraestrutura.
Perguntas frequentes
O que é um job em segundo plano?
É uma tarefa que sua aplicação roda fora do ciclo de requisição e resposta: o servidor enfileira o trabalho, responde ao usuário imediatamente, e um worker separado o executa de forma assíncrona — com retries se falhar. Tudo que é lento, repetível ou desnecessário para a resposta pertence ali.
Por que não fazer o trabalho direto no handler da requisição?
Porque trabalho lento bloqueia a resposta, prende capacidade do servidor e esbarra em timeouts de gateway — e se o trabalho der erro no meio da requisição, o usuário recebe uma falha mesmo com parte dele já feita. O exemplo canônico: enviar o e-mail de boas-vindas durante o cadastro, onde um servidor de e-mail lento transforma a criação da conta em um spinner.
Como funciona uma fila de jobs?
É uma lista durável entre produtores e workers: o app empurra um job — um tipo mais um payload pequeno —, a fila o persiste, e os workers puxam, executam e confirmam com um ack. Jobs sem ack voltam para a fila, e é daí que vem a confiabilidade.
Qual a diferença entre fila de jobs e fila de mensagens?
O propósito. Uma fila de mensagens move dados entre serviços — a entrega é o objetivo, e uma mensagem pode se espalhar para muitos consumidores. Uma fila de jobs executa trabalho — ela adiciona retries, agendamento, prioridades e status por cima, e cada job vai para exatamente um worker.
Qual a diferença entre cron jobs e jobs em segundo plano?
O gatilho. O cron é guiado por tempo — "toda noite às 2h"; jobs em segundo plano são guiados por eventos — "quando um usuário sobe um arquivo". Os padrões se compõem: um design comum tem o cron enfileirando o lote noturno enquanto os workers o executam com semântica completa de retry.
Como funcionam os retries de jobs?
Jobs que falharam rodam de novo automaticamente com backoff exponencial — um segundo, depois dois, quatro, oito — mais um jitter aleatório para que mil falhas não tentem de novo no mesmo instante, com um teto máximo de tentativas. O backoff atravessa falhas transitórias; o teto impede as permanentes de tentarem para sempre.
Por que jobs em segundo plano precisam ser idempotentes?
Porque filas prometem execução ao-menos-uma-vez: um worker pode travar depois de fazer o trabalho mas antes de confirmar o ack, e o job roda de novo. Rodar duas vezes não pode cobrar duas vezes — chaves de idempotência, constraints únicas e upserts transformam duplicatas em no-ops.
O que é uma fila dead-letter?
É para onde os jobs vão depois de esgotar seus retries — retidos para inspeção, alerta e reexecução manual, em vez de tentarem para sempre ou sumirem em silêncio. Jobs malformados que nunca vão ter sucesso deveriam pular os retries e ir direto para lá.