Encolar un background job
Declarar una queue, encolar trabajo, escribir un worker handler, monitorear con Bull Board.
Los background jobs usan BullMQ sobre Redis. El catálogo de queues es un set cerrado declarado en código para que productores y consumidores no puedan disentir sobre un nombre o forma de payload.
El catálogo
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;
}Un nombre de queue mal escrito falla en compile time. Una forma de payload incorrecta falla en compile time.
[!NOTE]
usage-period-rolloverse instala como tick recurrente (hourly @ :05 UTC) víaschedulePeriodRollover()al startup del server. El payload va vacío — el processor lee el tiempo del tick dejob.processedOnporque BullMQ persiste el payload de los jobs recurrentes al schedulear. Ver Usage metering para el ciclo de vida completo.
Política de retry por defecto
apps/server/src/infrastructure/jobs/queue-factory.ts setea las opciones 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 intentos con backoff exponencial anclado en 5 s, los jobs completados se barren después de 24 h, los fallidos se conservan por 7 días. Los productores pueden override por llamada.
1. Agregar una nueva 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;
}La queue se auto-registra para iteración de Bull Board.
2. Escribir el 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);
};Cableala en bootstrap/worker.ts para que el worker arranque con ese handler attacheado. El proceso del worker y el proceso de la API comparten la queue pero corren independientemente — la API encola, el worker drena.
3. Encolar desde un use case o listener
await deps.jobs.enqueue('report-exports', {
userId: input.userId,
reportId: input.reportId,
format: 'pdf',
});jobs es el port JobScheduler (ver infrastructure/jobs/bullmq-job-scheduler.adapter.ts). Encolar desde un listener de evento de dominio es el patrón más común.
Para override de retry options en una llamada específica:
await deps.jobs.enqueue('report-exports', payload, { attempts: 5, delay: 30_000 });4. Idempotencia
Los workers reintentan. Los handlers deben ser idempotentes. Dos patrones:
- Operación externa determinística: incluí una key que el tercero pueda dedupear (Stripe
Idempotency-Key, una fila de outbox keyada en el job id, etc.). - Escritura interna: gateá en una columna "processed at" o un unique index que absorba el segundo insert.
La queue de retry de webhooks de billing del boilerplate usa el id del evento del provider como idempotency key — los replays son no-op.
5. Monitorear
Bull Board se monta en /admin/queues cuando BULL_BOARD_ENABLED=true (default en dev, off en prod). Está protegido — necesitás una session autenticada con grants de admin para llegar.
Los counts y la profundidad por queue se envían a Prometheus vía el endpoint /metrics. Alertá sobre bullmq_failed_jobs_total subiendo o bullmq_waiting_jobs quedándose non-zero más tiempo del esperado.
Testing
Hacé unit-test del wrapper (el use case que llama jobs.enqueue) con un fake JobScheduler que registre llamadas. Los tests end-to-end de BullMQ necesitan un Redis real — BullMQ usa Lua + cmsgpack que ioredis-mock no puede correr. Testá la lógica del handler en aislamiento; reservá el end-to-end completo contra Redis para el set chico de jobs cuya interacción con Redis es la cosa bajo test.