Enviar e-mail, processar imagem, gerar relatório ou integrar um webhook dentro da requisição HTTP aumenta latência e torna a resposta dependente de serviços externos. O BullMQ no Node.js permite colocar esse trabalho em uma fila baseada em Redis, para que workers processem as tarefas de forma assíncrona, com retries, atraso, prioridade e observabilidade.
Uma fila não garante confiabilidade automaticamente. Jobs precisam ser idempotentes, workers devem encerrar corretamente e tentativas precisam de backoff. Também é necessário limitar concorrência, controlar retenção de histórico e monitorar tarefas atrasadas, falhas e filas acumuladas.
Neste guia, você aprenderá a criar Queue e Worker, adicionar jobs, configurar retries, definir prioridade, agendar tarefas, evitar duplicidade, organizar graceful shutdown e testar o processamento.
Instalando BullMQ
npm install bullmq ioredisO BullMQ usa Redis para armazenar estado e coordenar workers. A documentação oficial do BullMQ detalha classes, padrões e opções. A documentação do Redis explica estruturas de dados e persistência.
Para revisar cache e filas, consulte Redis com Node.js e o que é Node.js.
Criando a conexão Redis
const IORedis = require('ioredis');
const connection = new IORedis(process.env.REDIS_URL, {
maxRetriesPerRequest: null
});Workers de BullMQ exigem uma política de conexão compatível com operações bloqueantes. Valide TLS, autenticação e timeouts conforme o provedor. Não registre a URL completa se ela contém senha.
Criando uma fila
const { Queue } = require('bullmq');
const emailQueue = new Queue('emails', {
connection,
defaultJobOptions: {
attempts: 5,
backoff: {
type: 'exponential',
delay: 1000
},
removeOnComplete: 1000,
removeOnFail: 5000
}
});As opções padrão reduzem repetição de configuração. O histórico precisa de limite para o Redis não crescer indefinidamente.
Adicionando um job
await emailQueue.add('send-welcome', {
userId: user.id,
email: user.email
});O payload deve ser pequeno e serializável. Em vez de colocar um arquivo ou objeto enorme, armazene-o em banco ou object storage e envie apenas o identificador.
Criando um Worker
const { Worker } = require('bullmq');
const worker = new Worker('emails', async job => {
if (job.name === 'send-welcome') {
await sendWelcomeEmail(job.data);
return { sent: true };
}
throw new Error(`Job desconhecido: ${job.name}`);
}, {
connection,
concurrency: 10
});O worker busca jobs e executa a função. O valor retornado pode ser armazenado como resultado, mas evite respostas grandes.
Concorrência
concurrency define quantos jobs um worker processa simultaneamente. Para tarefas de rede, um valor maior pode melhorar vazão. Para CPU ou alto consumo de memória, ele pode piorar o desempenho.
Meça duração, CPU, memória e limites do serviço externo. Para tarefas computacionais, considere Worker Threads no Node.js dentro de uma arquitetura controlada.
Jobs idempotentes
Um worker pode executar novamente depois de timeout, falha de conexão ou reinício. O job não deve duplicar efeitos:
async function sendWelcomeEmail(job) {
const existing = await deliveries.findByJobId(job.id);
if (existing?.status === 'sent') {
return existing;
}
const delivery = await deliveries.reserve(job.id, job.data);
const result = await emailProvider.send(delivery.payload);
return deliveries.markSent(job.id, result.id);
}Use constraint única no banco para impedir duas reservas concorrentes.
jobId e deduplicação
await emailQueue.add('send-welcome', payload, {
jobId: `welcome:${user.id}`
});Um ID determinístico ajuda a evitar jobs duplicados enquanto o registro ainda existe. A política de remoção influencia por quanto tempo essa deduplicação permanece.
Retries e backoff
Configure tentativas apenas para falhas temporárias. Se o worker lançar erro de validação permanente, repetir cinco vezes desperdiça recursos. Uma estratégia é usar tipos de erro:
class PermanentJobError extends Error {}
async function processor(job) {
try {
return await execute(job.data);
} catch (error) {
if (error.code === 'INVALID_PAYLOAD') {
throw new PermanentJobError(error.message);
}
throw error;
}
}O tratamento exato depende das APIs da versão usada. O artigo sobre Retry com Backoff no Node.js explica jitter, orçamento e classificação de erros.
Jobs atrasados
await emailQueue.add('send-reminder', payload, {
delay: 60 * 60 * 1000
});O atraso não representa uma garantia de execução no milissegundo exato. Ele define quando o job se torna elegível, dependendo da capacidade dos workers.
Jobs recorrentes
Versões do BullMQ oferecem schedulers e opções para repetição. Defina identificadores estáveis e evite registrar o mesmo agendamento a cada inicialização sem deduplicação. Confirme a API da versão instalada, pois o modelo evoluiu ao longo das versões.
Prioridade
await queue.add('critical', payload, {
priority: 1
});
await queue.add('normal', payload, {
priority: 10
});Prioridade menor costuma representar execução mais urgente. Muitos níveis aumentam complexidade. Em alguns casos, filas separadas para classes diferentes são mais fáceis de dimensionar.
Progresso
await job.updateProgress(25);
Atualizações frequentes geram escrita adicional no Redis. Registre apenas marcos significativos, como 25%, 50% e 100%, em vez de cada item processado.
Eventos
worker.on('completed', job => {
logger.info({ jobId: job.id }, 'Job concluído');
});
worker.on('failed', (job, error) => {
logger.error({
jobId: job?.id,
attemptsMade: job?.attemptsMade,
error
}, 'Job falhou');
});
worker.on('error', error => {
logger.error({ error }, 'Erro do worker');
});Não dependa apenas de eventos locais para auditoria durável. O processo pode cair antes de registrar uma mensagem. Use o estado da fila e métricas.
QueueEvents
const { QueueEvents } = require('bullmq');
const queueEvents = new QueueEvents('emails', {
connection: new IORedis(process.env.REDIS_URL)
});Alguns componentes exigem conexão Redis separada. Planeje a quantidade total por processo e por instância.
Rate limit
Workers podem limitar a taxa para respeitar um provedor externo:
const worker = new Worker('emails', processor, {
connection,
limiter: {
max: 100,
duration: 60_000
}
});O limite da fila precisa combinar com o limite da API e com outras aplicações que usam a mesma conta. Veja Rate Limiting em APIs Node.js.
Timeout da tarefa
Não deixe jobs externos aguardarem indefinidamente. Use AbortSignal nas operações suportadas:
async function processJob(job) {
const signal = AbortSignal.timeout(30_000);
return callExternalService(job.data, { signal });
}Quando a função ignora o sinal, o timeout apenas da camada superior não libera o recurso subjacente.
Jobs travados
BullMQ detecta jobs cujo lock não foi renovado. Bloquear o event loop com CPU pode fazer um job parecer travado e ser executado novamente. Mantenha o processor assíncrono e mova trabalho pesado para workers de CPU.
Graceful shutdown
async function shutdown(signal) {
logger.info({ signal }, 'Fechando worker');
await worker.close();
await queueEvents.close();
await emailQueue.close();
await connection.quit();
}
worker.close() normalmente para novos jobs e aguarda o atual, conforme opções e versão. Defina um timeout global. Consulte Graceful Shutdown no Node.js.
Health checks
A readiness do produtor pode verificar Redis quando a fila é essencial. O worker pode expor estado de conexão e capacidade. Liveness não deve executar consultas pesadas. Veja Health Checks no Node.js.
Falha do Redis
Defina comportamento quando Redis fica indisponível. Uma rota que depende de enfileirar uma tarefa crítica deve falhar claramente, não fingir sucesso. Para tarefas opcionais, pode existir buffer local limitado, mas ele se perde no reinício e exige cautela.
Persistência do Redis
Se perder jobs é inaceitável, configure persistência, réplica e backup compatíveis com o SLA. BullMQ não substitui a operação correta do Redis.
Retenção e limpeza
removeOnComplete e removeOnFail podem aceitar quantidade ou idade, conforme a versão. Mantenha falhas tempo suficiente para investigação, sem deixar milhões de registros.
Observabilidade
Monitore:
- jobs waiting, active, delayed, completed e failed;
- idade do job mais antigo;
- duração por tipo;
- tentativas e esgotamento;
- workers ativos;
- uso de memória e latência do Redis.
Inclua IDs de correlação nos jobs e traces com OpenTelemetry no Node.js.
Como testar
Use Redis isolado e limpe a fila entre testes. Confirme processamento, retry, deduplicação, atraso, falha permanente e encerramento. Não use sleeps fixos; aguarde eventos com timeout ou consulte o estado.
Erros comuns
- Enviar payload enorme: Redis e serialização ficam sobrecarregados.
- Não tornar job idempotente: efeitos são duplicados.
- Concorrência alta demais: APIs e banco saturam.
- Retry em erro permanente: a fila acumula trabalho inútil.
- Não limitar histórico: memória do Redis cresce.
- Bloquear o event loop: locks expiram e jobs repetem.
- Encerrar processo sem close(): jobs ativos são interrompidos.
- Assumir execução exatamente uma vez: sistemas distribuídos exigem idempotência.
Boas práticas para produção
- Mantenha payloads pequenos.
- Use IDs determinísticos quando adequado.
- Projete jobs idempotentes.
- Classifique erros antes do retry.
- Aplique backoff e limite de tentativas.
- Dimensione concorrência pelas dependências.
- Defina rate limit e timeout.
- Limite retenção.
- Feche workers gradualmente.
- Monitore idade e falhas da fila.
Conclusão
O BullMQ no Node.js separa o recebimento de uma tarefa do processamento, usando Redis para coordenar filas e workers. A biblioteca oferece retries, atraso, prioridade, recorrência e eventos para fluxos assíncronos.
A confiabilidade depende do desenho do job. Payload pequeno, idempotência, concorrência limitada e graceful shutdown evitam duplicidade e sobrecarga. Com persistência e métricas adequadas, a fila permite executar trabalho em segundo plano sem comprometer a resposta da API.



