Delayed Jobs no Node.js representam tarefas que só devem ficar disponíveis depois de um instante específico. Lembretes, expiração de reservas, reprocessamentos, follow-ups e notificações programadas são exemplos comuns.
Um delay não garante execução no horário exato. Ele define o primeiro momento em que o job pode ser consumido. Atraso real depende da saúde do scheduler, capacidade dos workers, relógios, Redis ou banco e limites de concorrência.
Delay relativo
await queue.add('send-reminder', {
userId,
reminderId
}, {
delay: 30 * 60 * 1000,
jobId: `reminder:${reminderId}`
});O job fica elegível aproximadamente trinta minutos depois. Não use delay relativo para regras de calendário sem calcular o instante de negócio corretamente.
Horário absoluto
const runAt = new Date('2026-10-30T09:00:00-03:00');
const delay = Math.max(0, runAt.getTime() - Date.now());
await queue.add('send-reminder', data, { delay });Persista runAt em UTC e preserve o fuso original quando ele faz parte da regra.
Fonte de verdade
Não dependa apenas do estado interno da fila. Mantenha uma tabela de agendamentos.
CREATE TABLE scheduled_jobs (
id uuid PRIMARY KEY,
type text NOT NULL,
payload jsonb NOT NULL,
run_at timestamptz NOT NULL,
status text NOT NULL,
queue_job_id text,
version integer NOT NULL DEFAULT 1,
created_at timestamptz NOT NULL DEFAULT now()
);A tabela permite auditoria, cancelamento, rescheduling e recuperação caso o job desapareça da fila.
Padrão scheduler + queue
- a aplicação persiste o agendamento;
- um scheduler busca itens próximos;
- adiciona jobs com ID determinístico;
- marca o ID da fila;
- o worker revalida estado e horário;
- processa idempotentemente.
Busca de agendamentos
SELECT id, type, payload, run_at, version
FROM scheduled_jobs
WHERE status = 'scheduled'
AND run_at <= now() + interval '15 minutes'
ORDER BY run_at
FOR UPDATE SKIP LOCKED
LIMIT 100;Uma janela pequena reduz quantidade de jobs distantes mantidos na fila e facilita alterações.
Revalidando no worker
const schedule = await repository.findById(job.data.scheduleId);
if (!schedule || schedule.status !== 'scheduled') {
return { skipped: true };
}
if (schedule.version !== job.data.version) {
return { skipped: true, reason: 'superseded' };
}
if (schedule.runAt.getTime() > Date.now()) {
throw new RetryLaterError(schedule.runAt);
}O worker não deve confiar cegamente no payload antigo da fila.
Rescheduling
Ao alterar o horário:
- atualize
run_at; - incremente a versão;
- marque o job anterior como obsoleto ou remova se possível;
- agende o novo job;
- o worker antigo detecta versão diferente e ignora.
Cancelamento
UPDATE scheduled_jobs
SET status = 'cancelled', version = version + 1
WHERE id = $1 AND status = 'scheduled';Mesmo que o job já esteja ativo, ele deve verificar cancelamento antes do efeito externo.
Idempotência
Um delayed job pode ser executado mais de uma vez após falha ou timeout. Use uma chave de intenção.
const effectKey = `reminder:${schedule.id}:${schedule.version}`;Persista o efeito ou use constraint única no destino.
Precisão
Para lembretes, atraso de segundos costuma ser aceitável. Para leilões, reservas e pagamentos, a regra de negócio não deve depender apenas de um job que “fecha” no horário. O endpoint que recebe ações também precisa validar o prazo no banco.
Clock skew
Use relógios sincronizados e prefira a hora do banco para decisões críticas.
SELECT now();Calcular tudo em processos com relógios divergentes pode liberar jobs cedo ou tarde.
Retries atrasados
Retries e jobs agendados são conceitos diferentes. Um retry representa nova tentativa da mesma execução; um delayed job representa intenção futura. Registre ambos separadamente.
Backoff
Para erro transitório, calcule próxima tentativa com jitter. Não substitua o horário original sem guardar histórico.
Lotes de jobs no mesmo minuto
Campanhas e fechamentos podem criar milhares de jobs para o mesmo instante. Aplique dispersão controlada quando a regra permitir.
const jitter = Math.floor(Math.random() * 60_000);
const delay = baseDelay + jitter;Se o horário precisa ser exato, dimensione workers e dependências para o pico.
Multi-tenancy
Inclua tenant na chave e aplique quotas. Um tenant não deve preencher a fila e atrasar os demais.
Payload mínimo
Armazene IDs e versão, não uma cópia completa dos dados. Quando o job rodar, busque o estado atual e valide autorização e status.
Observabilidade
Monitore:
- diferença entre
run_ate início real; - jobs atrasados;
- agendamentos sem job;
- jobs sem agendamento;
- cancelamentos;
- reschedules;
- tentativas;
- idade do item mais atrasado.
Reconciliação
Execute periodicamente um job que encontra agendamentos vencidos ainda em estado scheduled e os reenvia para a fila. A chave determinística impede duplicação descontrolada.
Graceful shutdown
O scheduler deve parar de reservar novos itens e concluir as inserções em andamento. O worker termina ou libera jobs ativos de forma recuperável.
Testes
- delay relativo;
- horário absoluto e fuso;
- job atrasado;
- cancelamento antes e durante execução;
- rescheduling;
- versão obsoleta;
- clock skew;
- reconciliação;
- duplicação;
- pico de jobs.
Erros comuns
- usar fila como única fonte de verdade;
- achar que delay é execução exata;
- não revalidar estado;
- não versionar reschedule;
- cancelar apenas na fila;
- payload completo e obsoleto;
- sem reconciliação;
- ignorar pico no mesmo horário;
- não medir atraso real.
Checklist
run_atpersistido;- UTC e fuso definidos;
- ID determinístico;
- versão do agendamento;
- cancelamento persistido;
- worker revalida;
- idempotência;
- reconciliação;
- métricas de atraso;
- capacidade para picos.
Conclusão
Delayed Jobs no Node.js precisam de estado persistido e revalidação. A fila é um mecanismo de entrega, não a autoridade final sobre horário, cancelamento e versão.
Registre o agendamento, use IDs estáveis, reconcilie itens perdidos e meça atraso real. Com essa arquitetura, lembretes e expirações continuam corretos mesmo durante reinícios e falhas.
Consulte a documentação de delayed jobs do BullMQ e a documentação de Timers do Node.js.


