Orquestrador de Jobs em Segundo Plano
Especialista em projetar e implementar sistemas de jobs em segundo plano de nível de produção que lidam com tarefas de longa duração sem bloquear respostas de API.
Quando Usar
✅ Use para:
- Tarefas de longa duração (envio de e-mails, geração de relatórios, processamento de imagens)
- Operações em lote (importações em massa, exportações, migrações de dados)
- Tarefas agendadas (resumos diários, jobs de limpeza, relatórios recorrentes)
- Tarefas que exigem lógica de retentativa (chamadas de API externas, operações instáveis)
- Processamento baseado em prioridade (usuários premium primeiro, alertas críticos)
- Operações com limitação de taxa (cotas de API, limites de serviços de terceiros)
❌ NÃO para:
- Comunicação bidirecional em tempo real (use WebSockets)
- Requisitos de latência de submilissegundos (use cache em memória)
- Atrasos simples (setTimeout é suficiente para <5 segundos)
- Respostas síncronas de API (mantenha a lógica no manipulador de requisição)
Árvore de Decisão Rápida
Esta tarefa:
├── Leva >5 segundos? → Job em segundo plano
├── Precisa de retentativa em caso de falha? → Job em segundo plano
├── É executada em um agendamento? → Job em segundo plano (padrão cron)
├── Bloqueia a interação do usuário? → Job em segundo plano
├── Processa em lotes? → Job em segundo plano
└── Retorna imediatamente? → Mantenha síncrono
Seleção de Tecnologia
Node.js: BullMQ (Recomendado 2024+)
Quando usar:
- Projeto TypeScript
- Redis já na stack
- Precisa de recursos avançados (limitação de taxa, prioridades, jobs repetíveis)
Por que BullMQ em vez de Bull:
- Bull (v3) → BullMQ (v4+): Reescrita completa em TypeScript
- Melhor gerenciamento de conexão com Redis
- Concorrência e desempenho aprimorados
- Manutenção ativa (Bull está em modo de manutenção)
Python: Celery
Quando usar:
- Projeto Python/Django
- Precisa de execução distribuída de tarefas
- Fluxos de trabalho complexos (chains, groups, chords)
Alternativas:
- RQ (Redis Queue): Mais simples, menos recursos
- Dramatiq: Moderno, ecossistema menor
- Huey: Leve, bom para projetos pequenos
Nativo em Nuvem: AWS SQS, Google Cloud Tasks
Quando usar:
- Arquitetura serverless
- Não quer gerenciar Redis/RabbitMQ
- Precisa de entrega garantida e filas de mensagens mortas
Anti-Padrões Comuns
Anti-Padrão 1: Sem Fila de Mensagens Mortas
Pensamento iniciante: "Tentar 3 vezes, depois falhar silenciosamente"
Problema: Jobs com falha desaparecem sem visibilidade ou caminho de recuperação.
Abordagem correta:
// BullMQ com fila de mensagens mortas
const queue = new Queue('fila-email', {
connection: redis,
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
},
removeOnComplete: 100, // Manter últimos 100 bem-sucedidos
removeOnFail: false // Manter todas as falhas para inspeção
}
});
// Monitorar jobs com falha
const failedJobs = await queue.getFailed();
Linha do tempo:
- Pré-2020: Tentar e esquecer
- 2020+: Filas de mensagens mortas se tornam padrão
- 2024+: Observabilidade para falhas de jobs é obrigatória
Anti-Padrão 2: Processamento Síncrono de Jobs
Sintoma: Endpoint de API espera pela conclusão do job
Problema:
// ❌ ERRADO - Bloqueia a resposta da API
app.post('/enviar-email', async (req, res) => {
await sendEmail(req.body.para, req.body.assunto);
res.json({ success: true });
});
Por que está errado: Timeout, UX pobre, desperdício de recursos do servidor
Abordagem correta:
// ✅ CERTO - Enfileirar e retornar imediatamente
app.post('/enviar-email', async (req, res) => {
const job = await emailQueue.add('enviar', {
to: req.body.para,
subject: req.body.assunto
});
res.json({
success: true,
jobId: job.id,
status: 'enfileirado'
});
});
// Worker separado processa o job
worker.process('enviar', async (job) => {
await sendEmail(job.data.to, job.data.subject);
});
Anti-Padrão 3: Sem Idempotência
Problema: Job é executado duas vezes → cobranças duplicadas, e-mails duplicados
Por que acontece:
- Conexão Redis cai durante o processamento
- Worker trava antes da conclusão do job
- Timeout do job dispara retentativa enquanto ainda está em execução
Abordagem correta:
// ✅ Job idempotente com chave de desduplicação
await queue.add('cobrar-pagamento', {
userId: 123,
amount: 50.00
}, {
jobId: `payment-${orderId}`, // Previne duplicatas
attempts: 3
});
// No worker: Verificar se já foi processado
worker.process('cobrar-pagamento', async (job) => {
const { userId, amount } = job.data;
// Verificar idempotência
const existing = await db.payments.findOne({
jobId: job.id
});
if (existing) {
return existing; // Já processado
}
// Processar pagamento
const result = await stripe.charges.create({...});
// Armazenar registro de idempotência
await db.payments.create({
jobId: job.id,
result
});
return result;
});
Anti-Padrão 4: Sem Limitação de Taxa
Problema: Sobrecarregar APIs de terceiros ou esgotar cotas
Sintoma: Erros "Rate limit exceeded" do Sendgrid, Stripe, etc.
Abordagem correta:
// Limitação de taxa no BullMQ
const queue = new Queue('chamadas-api', {
limiter: {
max: 100, // Máximo 100 jobs
duration: 60000 // A cada 60 segundos
}
});
// Ou: Limites de taxa baseados em prioridade
await queue.add('enviar-email', data, {
priority: user.isPremium ? 1 : 10,
rateLimiter: {
max: user.isPremium ? 1000 : 100,
duration: 3600000 // Por hora
}
});
Anti-Padrão 5: Esquecer o Escalonamento de Workers
Problema: Um único worker não consegue acompanhar a profundidade da fila
Sintoma: Fila acumula, jobs atrasados por horas/dias
Abordagem correta:
// Escalonamento horizontal com múltiplos workers
const worker = new Worker('fila-email', async (job) => {
await processEmail(job.data);
}, {
connection: redis,
concurrency: 5 // Processar 5 jobs simultaneamente por worker
});
// Executar múltiplos processos de worker (PM2, Kubernetes, etc.)
// Cada worker processa concurrency * num_workers jobs
Monitoramento:
// Configurar alertas para profundidade da fila
setInterval(async () => {
const waiting = await queue.getWaitingCount();
if (waiting > 1000) {
alert('Profundidade da fila excede 1000, escalar workers!');
}
}, 60000);
Padrões de Implementação
Padrão 1: Campanhas de E-mail
// Configuração da fila
const emailQueue = new Queue('campanha-email', { connection: redis });
// Enfileirar lote
async function enviarCampanha(userIds: number[], template: string) {
const jobs = userIds.map(userId => ({
name: 'enviar',
data: { userId, template },
opts: {
attempts: 3,
backoff: { type: 'exponential', delay: 5000 }
}
}));
await emailQueue.addBulk(jobs);
}
// Worker com lógica de retentativa
const worker = new Worker('campanha-email', async (job) => {
const { userId, template } = job.data;
const user = await db.users.findById(userId);
const email = renderTemplate(template, user);
try {
await sendgrid.send({
to: user.email,
subject: email.subject,
html: email.body
});
} catch (error) {
if (error.code === 'ECONNREFUSED') {
throw error; // Tentar novamente
}
// E-mail inválido, não tentar novamente
console.error(`E-mail inválido para o usuário ${userId}`);
}
}, {
connection: redis,
concurrency: 10
});
Padrão 2: Relatórios Agendados
// Relatório diário às 9h
await queue.add('relatorio-diario', {
type: 'vendas',
recipients: ['[email protected]']
}, {
repeat: {
pattern: '0 9 * * *', // Sintaxe cron
tz: 'America/New_York'
}
});
// Worker gera e envia o relatório por e-mail
worker.process('relatorio-diario', async (job) => {
const { type, recipients } = job.data;
const data = await generateReport(type);
const pdf = await createPDF(data);
await emailQueue.add('enviar', {
to: recipients,
subject: `Relatório ${type} Diário`,
attachments: [{ filename: 'relatorio.pdf', content: pdf }]
});
});
Padrão 3: Pipeline de Transcodificação de Vídeo
// Job de múltiplos estágios com acompanhamento de progresso
await videoQueue.add('transcodificar', {
videoId: 123,
formats: ['720p', '1080p', '4k']
}, {
attempts: 2,
timeout: 3600000 // Timeout de 1 hora
});
worker.process('transcodificar', async (job) => {
const { videoId, formats } = job.data;
for (let i = 0; i < formats.length; i++) {
const format = formats[i];
// Atualizar progresso
await job.updateProgress((i / formats.length) * 100);
// Transcodificar
await ffmpeg.transcode(videoId, format);
}
await job.updateProgress(100);
});
// Cliente consulta o progresso
app.get('/videos/:id/status', async (req, res) => {
const job = await queue.getJob(req.params.jobId);
res.json({
state: await job.getState(),
progress: job.progress
});
});
Monitoramento e Observabilidade
Métricas Essenciais
// Dashboard de saúde da fila
async function getQueueMetrics() {
const [waiting, active, completed, failed, delayed] = await Promise.all([
queue.getWaitingCount(),
queue.getActiveCount(),
queue.getCompletedCount(),
queue.getFailedCount(),
queue.getDelayedCount()
]);
return {
waiting, // Jobs aguardando processamento
active, // Jobs em processamento
completed, // Concluídos com sucesso
failed, // Com falha após retentativas
delayed, // Agendados para o futuro
health: waiting < 1000 && failed < 100 ? 'saudável' : 'degradado'
};
}
BullMQ Board (Interface)
// Desenvolvimento: Monitorar jobs visualmente
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { ExpressAdapter } from '@bull-board/express';
const serverAdapter = new ExpressAdapter();
createBullBoard({
queues: [
new BullMQAdapter(emailQueue),
new BullMQAdapter(videoQueue)
],
serverAdapter
});
app.use('/admin/queues', serverAdapter.getRouter());
// Visite http://localhost:3000/admin/queues
Checklist de Produção
□ Fila de mensagens mortas configurada
□ Estratégia de retentativa com backoff exponencial
□ Limites de timeout de job definidos
□ Limitação de taxa para APIs de terceiros
□ Chaves de idempotência para operações críticas
□ Concorrência do worker ajustada (núcleos de CPU * 2)
□ Escalonamento horizontal configurado (vários workers)
□ Monitoramento da profundidade da fila com alertas
□ Fluxo de inspeção de jobs com falha
□ Dados do job não contêm PII nos logs
□ Persistência do Redis habilitada (AOF ou RDB)
□ Tratamento de desligamento gracioso (SIGTERM)
Quando Usar vs Evitar
| Cenário | Usar Jobs em Segundo Plano? |
|---|---|
| Enviar e-mail de boas-vindas ao se inscrever | ✅ Sim - pode levar de 2 a 5 segundos |
| Cobrar cartão de crédito | ⚠️ Talvez - depende da latência do provedor de pagamento |
| Gerar relatório PDF (30 segundos) | ✅ Sim - definitivamente em segundo plano |
| Buscar perfil de usuário do banco de dados | ❌ Não - milissegundos, mantenha síncrono |
| Processar upload de vídeo (5 minutos) | ✅ Sim - sempre em segundo plano |
| Validar entrada de formulário | ❌ Não - validação síncrona |
| Job cron diário | ✅ Sim - use jobs repetíveis |
| Mensagem de chat em tempo real | ❌ Não - use WebSockets |
Comparação de Tecnologias
| Recurso | BullMQ | Celery | AWS SQS |
|---|---|---|---|
| Linguagem | Node.js | Python | Qualquer (HTTP API) |
| Backend | Redis | Redis/RabbitMQ/SQS | Gerenciado |
| Prioridades | ✅ | ✅ | ✅ |
| Limitação de Taxa | ✅ | ❌ | ✅ (via atributos) |
| Repetição/Cron | ✅ | ✅ (celery-beat) | ❌ (use EventBridge) |
| Dashboard | Bull Board | Flower | CloudWatch |
| Fluxos de Trabalho | ❌ | ✅ (chains, groups) | ❌ |
| Curva de Aprendizado | Média | Média | Baixa |
| Custo | Hospedagem Redis | Hospedagem Redis | $0,40/milhão de requisições |
Referências
/references/bullmq-patterns.md- Padrões e exemplos avançados de BullMQ/references/celery-workflows.md- Chains, groups e chords do Celery/references/job-observability.md- Monitoramento, alertas e depuração
Scripts
scripts/setup_bullmq.sh- Inicializar BullMQ com Redisscripts/queue_health_check.ts- Dashboard de métricas da filascripts/retry_failed_jobs.ts- Re-tentar em lote jobs com falha
Este skill orienta: Implementação de jobs em segundo plano | Arquitetura de filas | Estratégias de retentativa | Escalonamento de workers | Observabilidade de jobs