Impedir que fluxos de trabalho do n8n processem o mesmo job duas vezes após uma falha do worker

Tenho uma configuração de n8n auto-hospedada onde workflows processam jobs armazenados em PostgreSQL. Estou usando múltiplos workers, e às vezes um worker falha ou perde a conexão enquanto processa um job.
O problema é que o job pode permanecer em processamento mesmo que o worker nunca o tenha concluído. Se eu simplesmente tentar novamente todos os jobs falhados, fico preocupado com dois workers processando o mesmo job simultaneamente.
Minha tabela atual se parece com:
CREATE TABLE jobs (
id BIGSERIAL PRIMARY KEY,
status TEXT NOT NULL,
locked_at TIMESTAMP,
worker_id TEXT,
updated_at TIMESTAMP DEFAULT NOW()
);
Estou considerando reivindicar jobs usando:
UPDATE jobs
SET
status = ‘processing’,
locked_at = NOW(),
worker_id = $1
WHERE id = $2
AND status = ‘pending’
RETURNING *;
Depois, periodicamente, redefinir jobs onde locked_at é mais antigo que um certo limite
Qual seria o padrão mais seguro em produção para reivindicação de jobs, falhas de workers e retries em n8n + PostgreSQL?
Seria SELECT … FOR UPDATE SKIP LOCKED melhor aqui, e como você evitaria que um worker recuperado/obsoleto atualizasse um job que já foi pego por outro worker?

Descreva o problema/erro/pergunta

Qual é a mensagem de erro (se houver)?

Por favor, compartilhe seu workflow

(Selecione os nós em sua tela e use os atalhos de teclado CMD+C/CTRL+C e CMD+V/CTRL+V para copiar e colar o workflow.)

Compartilhe a saída retornada pelo último nó

Informações sobre sua configuração de n8n

  • Versão do n8n:
  • Banco de dados (padrão: SQLite):
  • Configuração n8n EXECUTIONS_PROCESS (padrão: own, main):
  • Executando n8n via (Docker, npm, n8n cloud, app desktop):
  • Sistema operacional:

Oi @Gabby_Timothy

Para este caso, eu usaria FOR UPDATE SKIP LOCKED + um timeout de lease/lock + um token de worker.

A parte importante é garantir que um worker que falhou não mantenha um job permanentemente, ao mesmo tempo prevenindo que um worker antigo modifique um job que outro worker já recuperou.

Reivindicar jobs atomicamente

WITH job AS (
SELECT id
FROM jobs
WHERE status = ‘pending’
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE jobs
SET
status = ‘processing’,
locked_at = NOW(),
worker_id = $1,
updated_at = NOW()
FROM job
WHERE jobs.id = job.id
RETURNING jobs.*;

SKIP LOCKED é útil com múltiplos workers porque um worker não aguardará a linha bloqueada de outro worker.

Adicionar um timeout de lease

Se um worker falhar, o job pode eventualmente ficar disponível novamente: UPDATE jobs
SET
status = ‘pending’,
worker_id = NULL,
locked_at = NULL
WHERE status = ‘processing’
AND locked_at < NOW() - INTERVAL ‘10 minutes’;

O timeout deve ser maior que o tempo normal de processamento para evitar retentativas desnecessárias.

Proteger contra workers obsoletos

Eu não permitiria que um worker atualizasse um job usando apenas o id.

Use o token do worker como parte da condição: UPDATE jobs
SET
status = ‘completed’,
updated_at = NOW()
WHERE id = $1
AND status = ‘processing’
AND worker_id = $2;

Se o worker perdeu seu lease e outro worker recuperou o job, esta atualização afeta 0 linhas, então o worker obsoleto não pode sobrescrever o resultado do worker mais novo.

Padrão recomendado

Reivindicar job → processar → heartbeat/estender lease se necessário
→ completar → tentar novamente jobs expirados

Eu manteria a lógica de bloqueio real no PostgreSQL e deixaria o n8n lidar com a orquestração.

A ideia principal é: não confie no worker para saber se ainda é dono do job — o banco de dados deve decidir.

Oi @Gabby_Timothy
No modo fila, a duplicação pode vir do próprio n8n, antes de qualquer lógica de reclamação ser executada. Cada worker mantém um lease do Redis na execução por QUEUE_WORKER_LOCK_DURATION (60000 ms) e o renova a cada QUEUE_WORKER_LOCK_RENEW_TIME. Se um worker falhar ou estiver muito ocupado para renovar no tempo, a execução é marcada como travada e reentregue a outro worker, que executa o workflow novamente a partir do gatilho. No n8n 1.x essa repetição acontece uma vez por padrão, então desative-a e defina o lease acima do seu trabalho mais lento:

QUEUE_WORKER_MAX_STALLED_COUNT=0
QUEUE_WORKER_LOCK_DURATION=300000

No n8n 2.0 a variável e a repetição travada do Bull foram removidas, então remova a primeira linha após atualizar e dirija as tentativas de sua própria tabela.