SaaS Starter

Enfileirar um background job

Declarar uma queue, enfileirar trabalho, escrever um worker handler, monitorar com Bull Board.

Última atualização 19 de jul. de 2026

Background jobs usam BullMQ sobre Redis. O catálogo de queues é um set fechado declarado em código para que produtores e consumidores não possam discordar sobre um nome ou forma de payload.

apps/server/src/infrastructure/jobs/queues.ts:

export const QUEUE_NAMES = {
  emails: 'emails',
  webhooksOutgoing: 'webhooks-outgoing',
  billingWebhooksRetry: 'billing-webhooks-retry',
  accountDeletions: 'account-deletions',
  dataExports: 'data-exports',
  usagePeriodRollover: 'usage-period-rollover',
} as const;

export interface JobDataMap {
  emails: EmailJobData;
  'webhooks-outgoing': WebhooksOutgoingJobData;
  'billing-webhooks-retry': BillingWebhooksRetryJobData;
  'account-deletions': AccountDeletionJobData;
  'data-exports': DataExportJobData;
  'usage-period-rollover': UsagePeriodRolloverJobData;
}

Um nome de queue digitado errado falha em compile time. Uma forma de payload errada falha em compile time.

[!NOTE] usage-period-rollover é instalado como tick recorrente (hourly @ :05 UTC) via schedulePeriodRollover() no startup do server. O payload vai vazio — o processor lê o tempo do tick de job.processedOn porque BullMQ persiste o payload dos jobs recorrentes no momento do schedule. Ver Usage metering para o ciclo de vida completo.

Política de retry padrão

apps/server/src/infrastructure/jobs/queue-factory.ts define as opções default por job:

export const DEFAULT_JOB_OPTIONS: JobsOptions = {
  attempts: 3,
  backoff: { type: 'exponential', delay: 5_000 },
  removeOnComplete: { age: 60 * 60 * 24, count: 1_000 },
  removeOnFail: { age: 60 * 60 * 24 * 7 },
};

3 tentativas com backoff exponencial ancorado em 5 s, jobs completos varridos após 24 h, jobs falhos mantidos por 7 dias. Produtores podem fazer override por chamada.

1. Adicionar uma nova queue

// apps/server/src/infrastructure/jobs/queues.ts
export const QUEUE_NAMES = {
  /* ... */,
  reportExports: 'report-exports',
} as const;

export interface ReportExportJobData {
  readonly userId: string;
  readonly reportId: string;
  readonly format: 'csv' | 'pdf';
}

export interface JobDataMap {
  /* ... */,
  'report-exports': ReportExportJobData;
}

A queue é auto-registrada para iteração no Bull Board.

2. Escrever o worker handler

apps/server/src/infrastructure/jobs/processors/report-exports.processor.ts:

import type { Processor } from 'bullmq';
import type { ReportExportJobData } from '../queues.js';

export const reportExportsProcessor =
  (deps: { logger: Logger; reports: ReportsService }): Processor<ReportExportJobData> =>
  async (job) => {
    deps.logger.info({ jobId: job.id }, 'export starting');
    await deps.reports.export(job.data);
  };

Cabeie em bootstrap/worker.ts para que o worker suba com esse handler anexado. O processo do worker e o processo da API compartilham a queue mas rodam independentemente — a API enfileira, o worker drena.

3. Enfileirar de um use case ou listener

await deps.jobs.enqueue('report-exports', {
  userId: input.userId,
  reportId: input.reportId,
  format: 'pdf',
});

jobs é o port JobScheduler (veja infrastructure/jobs/bullmq-job-scheduler.adapter.ts). Enfileirar a partir de um listener de evento de domínio é o padrão mais comum.

Para fazer override de retry options numa chamada específica:

await deps.jobs.enqueue('report-exports', payload, { attempts: 5, delay: 30_000 });

4. Idempotência

Workers tentam de novo. Handlers precisam ser idempotentes. Dois padrões:

  • Operação externa determinística: inclua uma key que o terceiro consiga deduplicar (Stripe Idempotency-Key, uma linha de outbox keyada no job id, etc.).
  • Escrita interna: gate numa coluna "processed at" ou num unique index que absorva o segundo insert.

A queue de retry de webhooks de billing do boilerplate usa o id do evento do provider como idempotency key — replays são no-op.

5. Monitorar

Bull Board é montado em /admin/queues quando BULL_BOARD_ENABLED=true (default em dev, off em prod). É protegido por requirePlatformAdmin — você precisa de uma conta platform-admin (user.platformAdmin) para alcançar; uma session autenticada comum não basta.

Counts e profundidade por queue são enviados ao Prometheus via o endpoint /metrics. Alerte em bullmq_failed_jobs_total subindo ou bullmq_waiting_jobs ficando non-zero por mais tempo do que o esperado.

Testing

Faça unit-test do wrapper (o use case que chama jobs.enqueue) com um JobScheduler fake que grava chamadas. Testes end-to-end de BullMQ precisam de um Redis real — BullMQ usa Lua + cmsgpack que ioredis-mock não consegue rodar. Teste a lógica do handler isoladamente; reserve o end-to-end completo contra Redis para o pequeno conjunto de jobs cuja interação com Redis é a coisa sob teste.

Nesta página