Ao desenvolver com Node.js, o tratamento de erros parece quase fácil demais no início. Você envolve uma rota em um try-catch, envia um código de status 500 e o cliente decide o que fazer a seguir. Esse modelo funciona bem para HTTP, mas desmorona no momento em que você passa para jobs em segundo plano (background jobs). Em um sistema de fila, não há um cliente esperando. Existe apenas um worker, um payload e um contador de tentativas aumentando em algum lugar no Redis, RabbitMQ ou SQS. Se você tratar as falhas da mesma forma que trata requisições web que falharam, você não apenas perderá uma transação. Você travará todo o seu pipeline, consumirá recursos computacionais excessivamente ou derrubará seus workers repetidamente com a mesma mensagem envenenada (poisoned message).

A mentalidade HTTP falha em background jobs

Em um ciclo de requisição-resposta, o loop de feedback é imediato. Um usuário clica em um botão, o servidor lança um erro e o usuário vê uma tela de falha. A limpeza é simples. Um worker de fila vive isoladamente. Ele puxa um job, trabalha nele por segundos ou minutos e, em seguida, confirma o sucesso (acknowledgment). Se algo der errado no meio do caminho, a fila não tem ideia do porquê. Ela só sabe que a confirmação nunca chegou. Dependendo da sua configuração, ela tentará novamente, talvez para sempre. Um único payload malformado pode saltar entre workers centenas de vezes, desperdiçando CPU e escondendo-se atrás de jobs legítimos que realmente precisam de processamento.

Dois tipos de falha

A primeira regra de filas resilientes é parar de tratar cada erro da mesma maneira. Você precisa classificar as falhas em dois grupos no instante em que elas ocorrem.

Falhas de tentativa (retryable failures) são transitórias. Pense em timeouts de rede, limites de taxa (rate limits) de uma API de terceiros ou uma conexão de banco de dados que foi resetada porque o pool foi brevemente esgotado. Esses são sintomas de um sistema vivo sob pressão. Elas podem ter sucesso na próxima tentativa, daqui a dois minutos.

Falhas permanentes são "poison pills" (pílulas de veneno). Elas incluem payloads malformados, erros de validação de schema ou um campo obrigatório ausente porque um serviço upstream alterou seu contrato. Tentar novamente nesses casos é puro desperdício. Elas falharão de forma idêntica na centésima tentativa.

Se o seu bloco catch não consegue distinguir entre essas duas, sua fila está voando às cegas.

Padrão 1: Classifique os erros no bloco catch

O bloco catch do seu worker deve ser o código mais deliberado do arquivo. Quando um erro surge, inspecione-o imediatamente. O código de erro é ECONNRESET ou um timeout? Coloque na fila para nova tentativa. É um SyntaxError, uma rejeição de validação do Joi ou uma restrição de chave estrangeira ausente? Mova-o diretamente para uma dead-letter queue, ou DLQ, e não o conte contra o seu limite de tentativas.

A maioria das bibliotecas de fila do Node.js, incluindo BullMQ e Bee Queue, permite definir estratégias de backoff personalizadas e hooks de erro. Use-os. Um erro permanente nunca deve esperar e tentar novamente três vezes por padrão. Ele deve ser evacuado da fila principal para que o restante dos seus jobs possa fluir. A DLQ preserva o payload exato e o contexto do erro, o que permite que você execute o job novamente mais tarde, após corrigir o bug ou o schema.

Padrão 2: Exponential Backoff com Jitter

Tentar novamente de forma instantânea é agressivo. Se um banco de dados downstream já estiver sofrendo sob carga, atingi-lo novamente a cada dois segundos a partir de cinquenta workers irá finalizá-lo. Você precisa recuar e dar espaço para o sistema se recuperar.

Use exponential backoff. Na primeira falha, espere um segundo. Na segunda, espere dois. Depois quatro, depois oito, até um limite razoável como cinco minutos. Mas o tempo sozinho não é suficiente. Se cada job que falhou usar exatamente o mesmo intervalo, todos eles colidirão quando o backoff expirar. Essa onda sincronizada, às vezes chamada de "thundering herd" (manada estrondosa), pode sobrecarregar um serviço em recuperação.

Adicione jitter. Pegue o seu atraso calculado e aplique uma variação por uma porcentagem aleatória, talvez de dez a vinte por cento. Quatro segundos tornam-se 4,2 ou 4,7. Essa aleatoriedade simples espalha o pico de tentativas e evita que sua infraestrutura receba golpes em ondas.

Padrão 3: Projete para Idempotência

É aqui que a infraestrutura de fila encontra a lógica de negócio. Imagine um job que cobra um cliente através de um provedor de pagamento. O worker publica a cobrança com sucesso, mas a conexão cai antes que ele possa registrar o sucesso no seu banco de dados ou confirmar o job. A fila vê uma falha. Ela tenta novamente. O cliente é cobrado duas vezes.

In Node.js, prevent this by making every side effect idempotent. Generate an idempotency key from the job ID or a business-specific identifier. Before you create the charge or send the email or adjust inventory, check whether the work was already done. Pass that key all the way through to your database and to any third-party APIs that accept one. Structure your jobs so that running the same payload ten times produces the same outcome as running it once. This single habit eliminates an entire category of financial and data-integrity bugs.

Pattern 4: Treat Your Dead-Letter Queue Like a Dashboard

A DLQ is not a graveyard where bad jobs go to be forgotten. It is an operational tool, and it should be one of your most watched surfaces.

Set up alerts that fire when the DLQ depth increases. Even a single message in the DLQ often means your validation logic is broken, an upstream schema changed, or a downstream service is sending garbage you no longer recognize. These are exactly the signals you want to catch before customers start complaining. Build a dashboard that lets you inspect the raw payload, the stack trace, and the timestamp. Have a runbook ready: inspect the failure, patch the code, then replay the messages in the correct order. If your queue implementation supports it, alert on growth rate as well as absolute count, because one bad deploy can flood the DLQ within minutes.

Pattern 5: When in Doubt, Crash the Process

Node.js runs on a single event loop inside a V8 isolate. An unhandled promise rejection or an