Job Deduplication no Node.js impede que a mesma intenção de trabalho seja enfileirada ou processada repetidamente. Duplicações podem surgir por retries HTTP, múltiplas instâncias, eventos repetidos, reconciliação, quedas de conexão e cliques duplicados.
Deduplicação não é um único recurso da fila. Ela envolve uma chave estável, janela temporal, persistência e idempotência do efeito final. Mesmo que a fila rejeite jobs duplicados, o worker precisa suportar reexecução.
Defina a intenção
A primeira pergunta é: o que significa “o mesmo job”?
- enviar o e-mail de boas-vindas para um usuário;
- gerar relatório do tenant para um período;
- processar um evento externo específico;
- sincronizar um recurso na versão 18;
- fechar uma fatura mensal.
A chave deve representar essa intenção, não parâmetros incidentais como timestamp da requisição.
Chave determinística
function jobKey({ type, tenantId, resourceId, version }) {
return `${type}:${tenantId}:${resourceId}:${version}`;
}Normalize valores antes de formar a chave. Diferenças de maiúsculas, ordem ou espaços podem criar chaves distintas para a mesma operação.
jobId no BullMQ
await queue.add('generate-report', {
tenantId,
period
}, {
jobId: `report:${tenantId}:${period}`
});Enquanto o job com esse ID existir, adicionar outro com o mesmo ID não cria uma nova entrada. A retenção de jobs concluídos influencia por quanto tempo a deduplicação permanece.
Consulte BullMQ no Node.js.
Deduplicação persistida no banco
CREATE TABLE job_intents (
dedup_key text PRIMARY KEY,
job_type text NOT NULL,
payload jsonb NOT NULL,
status text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
completed_at timestamptz
);A aplicação tenta inserir antes de enfileirar.
const result = await client.query(
`INSERT INTO job_intents (dedup_key, job_type, payload, status)
VALUES ($1, $2, $3::jsonb, 'pending')
ON CONFLICT DO NOTHING
RETURNING dedup_key`,
[dedupKey, type, JSON.stringify(payload)]
);
if (result.rowCount === 0) {
return { duplicate: true };
}Falha entre banco e fila
Se a intenção é persistida e a aplicação cai antes de adicionar à fila, o job fica órfão. Resolva com Outbox ou reconciliação.
await client.query('BEGIN');
await createBusinessChange(client);
await createJobOutbox(client, { dedupKey, type, payload });
await client.query('COMMIT');Um dispatcher publica e marca a Outbox. Consulte Outbox Pattern no Node.js.
Deduplicação por janela
Algumas intenções podem repetir depois de um período.
const bucket = Math.floor(Date.now() / (5 * 60 * 1000));
const key = `refresh-dashboard:${tenantId}:${bucket}`;A janela precisa ser parte explícita da regra. Não use TTL arbitrário sem entender se um segundo job legítimo pode ocorrer.
Debounce
Debounce adia o processamento para agrupar mudanças frequentes. Por exemplo, gerar uma prévia 30 segundos depois da última edição.
Ao receber nova mudança:
- atualize a versão ou payload persistido;
- reagende o job;
- o worker verifica se ainda é a versão mais nova;
- jobs antigos são ignorados.
Throttle
Throttle permite no máximo uma execução por intervalo, mas não necessariamente usa o último estado. Escolha entre debounce e throttle conforme o produto.
Deduplicação de eventos
Eventos externos devem ter ID do emissor.
CREATE TABLE inbox (
source text NOT NULL,
event_id text NOT NULL,
received_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (source, event_id)
);Se o insert conflita, o evento já foi recebido. Veja Consumer Idempotente no Node.js.
Fingerprint de payload
Quando não existe ID confiável, derive um hash da intenção normalizada.
import { createHash } from 'node:crypto';
function fingerprint(value) {
return createHash('sha256')
.update(stableStringify(value))
.digest('hex');
}JSON.stringify comum depende da ordem de propriedades. Use serialização canônica e inclua tipo, versão e escopo.
Colisões e segurança
SHA-256 oferece risco de colisão desprezível para esse uso, mas a chave ainda precisa incluir tenant e contexto. Não aceite uma dedup key enviada pelo usuário como autoridade sem validar.
Estado em processamento
Um job pode ficar marcado como active após queda. Use lease ou heartbeat quando o sistema próprio gerencia estado.
UPDATE job_intents
SET status = 'processing',
lease_until = now() + interval '5 minutes',
worker_id = $2
WHERE dedup_key = $1
AND (
status = 'pending'
OR (status = 'processing' AND lease_until < now())
);O lease permite recuperação. Ele não elimina idempotência do efeito externo.
Efeito final
A deduplicação mais importante ocorre perto do efeito:
- constraint única no banco;
- idempotency key em API externa;
- chave de mensagem enviada;
- versão do recurso;
- registro de notificação já enviada.
Consulte Idempotency Keys no Node.js.
Jobs diferentes com mesmo recurso
Não use somente o ID do recurso. “recalcular”, “exportar” e “notificar” são intenções diferentes.
recalculate:tenant-1:order-9:v3
export:tenant-1:order-9:pdf:v1
notify:tenant-1:order-9:paid:v2Cancelamento e rescheduling
Ao reagendar, incremente versão. O job anterior mantém a chave antiga e se torna obsoleto.
Veja Delayed Jobs no Node.js.
Retenção
Guardar chaves para sempre consome espaço. Defina retenção com base no período máximo em que uma duplicação ainda pode chegar. Eventos financeiros podem exigir retenção longa; atualizações de cache podem usar minutos.
Observabilidade
Monitore jobs deduplicados, chaves em conflito, idade de intents pendentes, leases expirados, reconciliações e duplicações que chegaram ao efeito final.
Testes concorrentes
Execute várias requisições simultâneas com a mesma chave e confirme que apenas uma intenção é criada. Teste queda entre persistência e fila, timeout do worker e retry após efeito concluído.
Erros comuns
- usar ID aleatório em toda tentativa;
- achar que jobId resolve para sempre;
- não incluir tenant;
- hash de JSON não canônico;
- TTL arbitrário;
- sem reconciliação;
- estado processing sem lease;
- deduplicar fila, mas não efeito;
- reutilizar chave para intenções diferentes.
Checklist
- intenção definida;
- chave determinística;
- tenant e versão incluídos;
- persistência atômica;
- Outbox ou reconciliação;
- lease para processamento;
- idempotência no efeito;
- retenção definida;
- métricas de conflitos;
- testes concorrentes.
Conclusão
Job Deduplication no Node.js começa com uma definição precisa da intenção. A chave deve representar o trabalho de negócio e permanecer estável entre retries e instâncias.
Use jobId como primeira camada, banco ou Inbox como registro durável e idempotência no efeito final. Com leases, retenção e reconciliação, duplicações deixam de ser uma surpresa operacional.
Consulte a documentação de deduplicação do BullMQ e a documentação de constraints do PostgreSQL.


