Advanced (Atualizado: 02/06/2026)

Sistema de filas com Claude Code: guia prático de processamento assíncrono

Projete producers, workers, retries, DLQ, idempotência e monitoramento com Claude Code.

Sistema de filas com Claude Code: guia prático de processamento assíncrono

Ao usar Claude Code para criar uma aplicação web, é comum colocar tudo dentro do handler HTTP. O formulário envia e-mail antes de responder, o upload de imagem gera thumbnails no mesmo request, e o webhook de pagamento atualiza pedido, fatura, e-mail e CRM em uma única rota. Isso funciona em demonstrações. Em produção entram timeouts, requests duplicados, reinícios de deploy, limites de provedores externos e falhas parciais.

Um sistema de filas separa o request visível para o usuário do trabalho lento ou frágil. O producer coloca um job na fila, o consumer ou worker pega esse job, lê o message payload, executa o efeito, confirma sucesso, tenta novamente falhas temporárias e envia falhas repetidas para uma dead-letter queue, ou DLQ. Também é preciso definir visibility timeout, o tempo em que um job fica invisível para outros workers enquanto está sendo processado; idempotência, para que o mesmo job não produza efeito de negócio duplicado; backpressure, para reduzir entrada quando os workers não acompanham; e monitoramento, para enxergar acúmulo e erros.

Os exemplos deste artigo são scripts Node.js sem dependências. Você não precisa de Redis, AWS ou RabbitMQ para copiar e executar. O objetivo é entender o contrato operacional antes de escolher SQS, RabbitMQ, BullMQ ou outro broker.

Visão geral

Fila não é apenas “rodar em background”. Ela desacopla sistemas, protege serviços externos, limita concorrência, isola falhas e cria evidência para operação.

flowchart LR
  A["Producer<br/>API, cron, webhook"] --> B["Queue<br/>message payload"]
  B --> C["Consumer<br/>worker process"]
  C --> D["External service<br/>mail, image, billing"]
  C -- "retryable failure" --> B
  C -- "poison message" --> E["DLQ<br/>manual review"]
  C --> F["Metrics<br/>logs and alerts"]
TermoSignificado simplesDecisão de projeto
ProducerCódigo que coloca trabalho na filaFormato do payload, validação, prioridade, chave de deduplicação
ConsumerWorker que pega e executa jobsConcorrência, timeout, comportamento em falha
Message payloadDados lidos pelo workerIDs, tipo, versão de schema, sem secrets
Visibility timeoutTempo em que um job pego fica ocultoUm pouco acima do p95 de processamento
RetryTentar novamente falhas temporáriasMáximo de tentativas, backoff, jitter, motivo da falha
DLQFila para jobs que não devem continuar em retry automáticoDono, alerta, regras de replay
IdempotênciaRepetir o job sem duplicar resultado de negócioChave única, tabela de processados
BackpressureReduzir entrada quando falta capacidadeLimite de concorrência, rate limit, profundidade da fila
MonitoramentoEvidência de saúde ou travamentoProfundidade, job mais antigo, taxa de falha, contagem DLQ

Essa tabela deve aparecer no prompt para Claude Code. Ela obriga a implementação a tratar falhas, e não apenas o caminho feliz.

Casos de uso

O primeiro caso é envio de e-mail. Boas-vindas, reset de senha, alerta de cobrança e resposta de suporte não devem bloquear a resposta HTTP. Para detalhes, veja automação de e-mail e envio com SendGrid. O payload deve conter deliveryId, templateId e userId, não API key, token ou corpo completo do e-mail.

O segundo caso é processamento de imagem e vídeo. Gerar thumbnails, converter para WebP, escanear vírus, criar legendas ou montar previews consome CPU e tempo. A fila permite responder “recebido” rapidamente e processar com concorrência controlada. O risco comum é deixar workers ilimitados.

O terceiro caso é retry de cobrança. Provedores de pagamento, redes de cartão e emissão de notas podem falhar temporariamente. Retry ajuda, mas precisa ser finito. Sem idempotência, backoff e DLQ, você pode gerar cobrança duplicada ou sobrecarregar um provedor que já está instável.

O quarto caso é enriquecimento de leads e geração de relatórios. Depois de um formulário, você pode enriquecer dados da empresa, escrever no CRM, gerar relatório comercial e avisar no Slack. Esse desenho conversa com arquitetura orientada a eventos, logging e monitoramento e boas práticas de segurança.

Exemplo 1: fila em memória sem dependências

Salve como queue-basic-demo.mjs e rode node queue-basic-demo.mjs. O script mostra producer, consumer, payload, visibility timeout e backpressure. Ele não é produção porque tudo está em memória, mas torna o ciclo de vida visível.

// queue-basic-demo.mjs
let nextJobId = 1;

class InMemoryQueue {
  constructor({ visibilityTimeoutMs = 800, maxInFlight = 2 } = {}) {
    this.visibilityTimeoutMs = visibilityTimeoutMs;
    this.maxInFlight = maxInFlight;
    this.ready = [];
    this.inFlight = new Map();
  }

  enqueue(type, payload) {
    const job = {
      id: `job-${nextJobId++}`,
      type,
      payload,
      attempts: 0,
      visibleAt: 0,
      lockedBy: null,
    };
    this.ready.push(job);
    return job.id;
  }

  receive(workerId) {
    this.requeueExpired();

    if (this.inFlight.size >= this.maxInFlight) {
      return null;
    }

    const job = this.ready.shift();
    if (!job) return null;

    job.attempts += 1;
    job.lockedBy = workerId;
    job.visibleAt = Date.now() + this.visibilityTimeoutMs;
    this.inFlight.set(job.id, job);

    return {
      id: job.id,
      type: job.type,
      payload: job.payload,
      attempts: job.attempts,
    };
  }

  ack(jobId) {
    this.inFlight.delete(jobId);
  }

  requeueExpired(now = Date.now()) {
    for (const [jobId, job] of this.inFlight.entries()) {
      if (job.visibleAt <= now) {
        this.inFlight.delete(jobId);
        job.lockedBy = null;
        this.ready.push(job);
      }
    }
  }

  stats() {
    this.requeueExpired();
    return {
      ready: this.ready.length,
      inFlight: this.inFlight.size,
    };
  }
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function produce(queue) {
  queue.enqueue("email.send", {
    deliveryId: "mail-1001",
    templateId: "welcome",
    userId: "user-42",
  });
  queue.enqueue("image.resize", {
    assetId: "asset-9001",
    sizes: [320, 768, 1280],
  });
  queue.enqueue("report.generate", {
    reportId: "weekly-2026-06-02",
    accountId: "acct-7",
  });
}

async function consume(queue, workerId) {
  for (let step = 0; step < 8; step += 1) {
    const job = queue.receive(workerId);

    if (!job) {
      console.log(`${workerId}: no job or backpressure`, queue.stats());
      await sleep(120);
      continue;
    }

    console.log(`${workerId}: started ${job.id}`, job.payload);
    await sleep(job.type === "image.resize" ? 300 : 90);
    queue.ack(job.id);
    console.log(`${workerId}: acked ${job.id}`, queue.stats());
  }
}

async function main() {
  const queue = new InMemoryQueue({
    visibilityTimeoutMs: 500,
    maxInFlight: 2,
  });

  produce(queue);
  await Promise.all([consume(queue, "worker-a"), consume(queue, "worker-b")]);
  console.log("final stats", queue.stats());
}

void main();

Em produção, o array ready vira SQS, RabbitMQ, Redis ou outro serviço persistente. O modelo continua igual: pronto, em processamento, confirmado ou devolvido após timeout.

Exemplo 2: guarda de idempotência no worker

A maioria das filas trabalha com entrega pelo menos uma vez. O mesmo job pode voltar. Sem idempotência, e-mail, cobrança, pontos e CRM podem ser duplicados.

// idempotent-worker-demo.mjs
const idempotencyStore = new Map();
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function withIdempotency(key, work) {
  const current = idempotencyStore.get(key);

  if (current?.status === "done") {
    return { skipped: true, result: current.result };
  }

  if (current?.status === "processing") {
    return { skipped: true, reason: "already processing" };
  }

  idempotencyStore.set(key, { status: "processing" });

  try {
    const result = await work();
    idempotencyStore.set(key, { status: "done", result });
    return { skipped: false, result };
  } catch (error) {
    idempotencyStore.delete(key);
    throw error;
  }
}

async function fakeSendEmail(payload) {
  await sleep(50);
  return {
    providerMessageId: `sg_${payload.deliveryId}`,
    sentToUserId: payload.userId,
  };
}

async function handleEmailJob(job) {
  const key = job.payload.idempotencyKey;
  if (!key) throw new Error("missing idempotencyKey");

  return withIdempotency(key, () => fakeSendEmail(job.payload));
}

async function main() {
  const original = {
    id: "job-1",
    payload: {
      idempotencyKey: "email:welcome:user-42",
      deliveryId: "mail-1001",
      userId: "user-42",
    },
  };

  console.log(await handleEmailJob(original));
  console.log(await handleEmailJob({ ...original, id: "job-1-redelivery" }));
}

void main();

Em produção, troque o Map por unique constraint no banco, Redis SETNX ou idempotency key do provedor. Peça ao Claude Code para marcar sucesso só depois do efeito externo, liberar lock em falha e nunca colocar secrets no payload.

Exemplo 3: retry e DLQ

Retry serve para erro temporário. Não resolve payload inválido, usuário removido, permissão errada ou configuração ausente. Poison message é um job que continuará falhando até alguém corrigir a causa.

// retry-dlq-demo.mjs
let nextRetryJobId = 1;

class RetryQueue {
  constructor({ maxAttempts = 3 } = {}) {
    this.maxAttempts = maxAttempts;
    this.ready = [];
    this.delayed = [];
    this.dead = [];
    this.completed = [];
  }

  enqueue(payload) {
    this.ready.push({
      id: `retry-job-${nextRetryJobId++}`,
      payload,
      attempts: 0,
      runAt: Date.now(),
      lastError: null,
    });
  }

  moveReadyJobs(now = Date.now()) {
    const stillDelayed = [];
    for (const job of this.delayed) {
      if (job.runAt <= now) {
        this.ready.push(job);
      } else {
        stillDelayed.push(job);
      }
    }
    this.delayed = stillDelayed;
  }

  retryOrDeadLetter(job, error) {
    job.lastError = error.message;

    if (job.attempts >= this.maxAttempts) {
      this.dead.push(job);
      return;
    }

    const delayMs = 50 * 2 ** (job.attempts - 1);
    job.runAt = Date.now() + delayMs;
    this.delayed.push(job);
  }

  async drain(handler) {
    let idleRounds = 0;

    while (this.ready.length > 0 || this.delayed.length > 0) {
      this.moveReadyJobs();
      const job = this.ready.shift();

      if (!job) {
        idleRounds += 1;
        if (idleRounds > 100) throw new Error("drain timeout");
        await sleep(20);
        continue;
      }

      idleRounds = 0;
      job.attempts += 1;

      try {
        const result = await handler(job);
        this.completed.push({ id: job.id, result });
      } catch (error) {
        this.retryOrDeadLetter(job, error);
      }
    }

    return {
      completed: this.completed.length,
      dead: this.dead.length,
    };
  }
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function handler(job) {
  if (job.payload.kind === "poison") {
    throw new Error("invalid payload schema");
  }

  if (job.payload.kind === "flaky" && job.attempts < 2) {
    throw new Error("temporary provider timeout");
  }

  return `processed ${job.payload.kind}`;
}

async function main() {
  const queue = new RetryQueue({ maxAttempts: 3 });
  queue.enqueue({ kind: "normal" });
  queue.enqueue({ kind: "flaky" });
  queue.enqueue({ kind: "poison" });

  console.log(await queue.drain(handler));
  console.log(
    "dead letters",
    queue.dead.map((job) => ({
      id: job.id,
      attempts: job.attempts,
      lastError: job.lastError,
      payload: job.payload,
    }))
  );
}

void main();

DLQ precisa de dono. Alerta, motivo da falha, correção, replay e descarte precisam estar documentados. Sem isso, ela vira perda silenciosa.

Checklist operacional

  • Payload contém jobId, type, schemaVersion, ID de negócio e idempotency key.
  • Payload não contém API keys, OAuth tokens, cartão, corpo completo de e-mail ou dados pessoais extensos.
  • Producer valida antes de enfileirar.
  • Visibility timeout fica um pouco acima do p95 de processamento.
  • Retry, backoff, jitter e DLQ estão definidos antes de produção.
  • Concorrência do worker respeita conexões de DB, rate limits, CPU e memória.
  • Monitorar profundidade, job mais antigo, ativos, falhas, DLQ e p95.
  • Runbook cobre revisão, correção, replay e exclusão da DLQ.
  • E-mail, billing, pontos e CRM assumem entrega duplicada.
  • Review com Claude Code cobre caminhos de falha e logs.

Visibility timeout curto cria processamento duplicado. Longo demais esconde jobs quando worker cai. Meça p95 real e divida trabalhos longos.

Prompt para Claude Code

Descreva o contrato de falha:

Adicione uma fila de envio de e-mail. A API salva a solicitação e enfileira apenas deliveryId e templateId. O worker usa idempotency key para evitar envio duplicado, tenta erros temporários do provedor até 3 vezes com exponential backoff e move falhas repetidas para uma tabela DLQ. Não coloque API keys, corpo de e-mail ou dados pessoais no payload. Exponha queue depth, oldest job age, failure rate e DLQ count. Adicione testes para entrega duplicada, poison message e visibility timeout.

Esse prompt dá critérios de review para Claude Code, em vez de aceitar um exemplo mínimo sem operação.

Documentação oficial

Em infraestrutura AWS, comece pelo Amazon SQS Developer Guide. Para routing, exchanges, pub/sub e topologia flexível, veja RabbitMQ documentation. Se seu stack Node.js já usa Redis e você quer delayed jobs, repeatable jobs e boa ergonomia de worker, leia BullMQ documentation.

Escolha a ferramenta depois do contrato. Payload, idempotência, retry, DLQ, métricas, permissões, custo e experiência do time importam mais que o nome do pacote.

Armadilhas comuns

A primeira é duplicate processing. Filas geralmente garantem entrega ao menos uma vez, não efeito de negócio exatamente uma vez. A proteção deve estar no ponto do efeito.

A segunda é poison message. Schema antigo, usuário removido ou permissão errada não melhora com dez retries. Valide, registre motivo, mande para DLQ e só faça replay depois da correção.

A terceira é retry infinito. Durante queda de provedor, retry imediato amplia tráfego e atrasa recuperação. Use tentativas finitas, backoff, jitter e backpressure.

A quarta é guardar secrets no payload. Dados de fila aparecem em logs, DLQ, dashboards e suporte. Use IDs de referência e deixe o worker buscar dados sensíveis em fonte autorizada.

Treinamento e consultoria

Filas parecem simples no código e difíceis na operação. A ClaudeCodeLab pode transformar esta checklist em processo de time: prompts para Claude Code, regras em CLAUDE.md, schema de payload, runbook de DLQ, métricas e revisão CI. Para equipes, use treinamento e consultoria Claude Code. Para trabalho individual, cole a checklist no template de PR.

Resumo

Uma fila de jobs é infraestrutura de produção. Ela controla trabalho lento, isola falhas, evita efeitos duplicados, limita concorrência e deixa evidência operacional. Ao pedir uma fila ao Claude Code, inclua producer, consumer, payload, visibility timeout, retry, DLQ, idempotência, backpressure e monitoramento desde o primeiro prompt.

Resultado testado por Masa: executei os três exemplos Node.js localmente sem serviço externo e confirmei o fluxo básico, a proteção contra entrega duplicada e o envio de poison message para DLQ. O exemplo de idempotência foi especialmente útil como material de prompt, porque a segunda entrega do mesmo e-mail reutilizou o resultado salvo em vez de enviar novamente.

#Claude Code #filas de jobs #processamento assíncrono #BullMQ #Redis
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.