Advanced (Actualizado: 2/6/2026)

Sistema de colas con Claude Code: guía práctica de procesamiento asíncrono

Diseña colas con producers, workers, reintentos, DLQ, idempotencia y monitoreo en Claude Code.

Sistema de colas con Claude Code: guía práctica de procesamiento asíncrono

Cuando usas Claude Code para crear una aplicación web, es fácil meter todo dentro del handler HTTP: el formulario envía el correo antes de responder, la subida de imagen genera miniaturas en el mismo request, y el webhook de pago actualiza la base de datos, manda el recibo y escribe en el CRM. En una demo funciona. En producción aparecen timeouts, duplicados, reinicios de despliegue, límites de proveedores externos y errores parciales.

Un sistema de colas separa el request visible para el usuario del trabajo lento o frágil. El producer publica un job, el consumer o worker lo toma, procesa el message payload, confirma el éxito, reintenta fallos temporales y manda los fallos repetidos a una dead-letter queue o DLQ. También hay que definir visibility timeout, es decir, cuánto tiempo queda oculto un job mientras un worker lo procesa; idempotencia, que evita efectos duplicados si el mismo job llega dos veces; backpressure, que limita la entrada cuando los workers no alcanzan; y monitoreo, para saber si la cola se está acumulando.

Los ejemplos de este artículo son scripts Node.js sin dependencias. No necesitas Redis, AWS ni RabbitMQ para copiarlos y ejecutarlos. La idea es entender el contrato operativo antes de elegir SQS, RabbitMQ, BullMQ u otra herramienta de producción.

Mapa del sistema

Una cola no sirve solo para “hacerlo en background”. También protege a los servicios externos, controla la concurrencia, conserva evidencia de fallos y permite responder rápido al usuario aunque el trabajo real tarde más.

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"]
TérminoExplicación simpleDecisión de diseño
ProducerCódigo que encola trabajosForma del payload, validación, prioridad, clave de deduplicación
ConsumerWorker que toma y ejecuta trabajosConcurrencia, timeout, política de fallo
Message payloadDatos que lee el workerIDs, tipo, versión de schema, sin secretos
Visibility timeoutTiempo durante el cual un job tomado no se entrega a otro workerUn poco mayor que el p95 de procesamiento
RetryReejecución ante fallos temporalesMáximo de intentos, backoff, jitter, razón del fallo
DLQCola para trabajos que ya no deben reintentarse automáticamenteDueño, alerta, reglas de replay
IdempotenciaRepetir un job sin duplicar el resultado de negocioClave única, tabla de procesados, clave del proveedor
BackpressureFrenar entrada cuando falta capacidadLímite de concurrencia, rate limit, umbral de profundidad
MonitoreoEvidencia de salud de la colaProfundidad, job más antiguo, tasa de fallo, conteo DLQ

Este vocabulario debe aparecer en el prompt para Claude Code. Si solo pides “usa BullMQ”, recibirás una integración, pero no necesariamente un sistema operable.

Casos de uso

El primer caso es el envío de correo. Correos de bienvenida, restablecimiento de contraseña, recordatorios de factura y respuestas de soporte no deberían bloquear el request. Para profundizar, revisa automatización de email y email con SendGrid. El payload debe llevar deliveryId, templateId y userId; no debe llevar API keys, tokens ni el cuerpo completo del correo.

El segundo caso es procesamiento de imagen y video. Crear thumbnails, convertir a WebP, escanear archivos, generar subtítulos o preparar previews puede consumir CPU y memoria. La cola permite responder “aceptado” rápido y controlar cuántos workers procesan a la vez. El riesgo típico es permitir concurrencia ilimitada.

El tercer caso es reintento de billing. Proveedores de pago, redes de tarjetas y sistemas de facturación pueden fallar temporalmente. Una cola de reintentos ayuda, pero debe ser finita. Sin idempotencia y DLQ puedes terminar con cobros duplicados o trabajos que golpean el proveedor durante una caída.

El cuarto caso es enriquecimiento de leads y generación de reportes. Tras un formulario, puedes enriquecer datos de empresa, escribir en el CRM, generar un resumen comercial y avisar por Slack. El diseño general conecta con arquitectura orientada a eventos, la investigación de incidentes con logging y monitoring, y la seguridad del payload con buenas prácticas de seguridad.

Ejemplo 1: cola en memoria sin dependencias

Guarda este archivo como queue-basic-demo.mjs y ejecuta node queue-basic-demo.mjs. Muestra producer, consumer, payload, visibility timeout y backpressure. No es una cola de producción porque no persiste datos, pero sí reproduce los estados básicos.

// 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();

En producción, el arreglo ready sería SQS, RabbitMQ, Redis u otro broker duradero. El modelo mental no cambia: un job está listo, en proceso, confirmado o devuelto por timeout.

Ejemplo 2: idempotencia del worker

Muchas colas entregan al menos una vez. Eso implica que el mismo job puede llegar dos veces. Si el efecto de negocio no es idempotente, aparecerán correos duplicados, cobros dobles, puntos duplicados o registros repetidos en CRM.

// 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();

En producción, reemplaza el Map por una restricción única en base de datos, Redis SETNX o una clave de idempotencia del proveedor. Pide a Claude Code marcar como completado solo después del efecto externo, liberar el lock si falla y no guardar secretos en el payload.

Ejemplo 3: retry y DLQ

Retry sirve para fallos temporales. No arregla payloads inválidos, usuarios borrados, permisos incorrectos o configuración ausente. Un poison message es un trabajo que seguirá fallando hasta que alguien corrija la 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();

Una DLQ sin dueño no es operación, es una acumulación silenciosa. Debe tener alerta, responsable, razón de fallo, regla de replay y criterio para borrar o corregir.

Checklist operativo

  • El payload incluye jobId, type, schemaVersion, ID de negocio e idempotency key.
  • El payload no contiene API keys, tokens OAuth, datos de tarjeta, cuerpo completo de email ni texto personal extenso.
  • El producer valida antes de encolar.
  • El visibility timeout es un poco mayor que el p95 de procesamiento.
  • Retry, backoff, jitter y DLQ están definidos antes de producción.
  • La concurrencia del worker respeta conexiones de DB, rate limits, CPU y memoria.
  • Se monitorean profundidad, job más antiguo, activos, fallos, DLQ y p95.
  • Existe runbook para revisar, corregir y reintentar trabajos en DLQ.
  • Email, billing, puntos y CRM asumen entrega duplicada.
  • La revisión con Claude Code cubre fallos, no solo el camino feliz.

El visibility timeout suele fallar por exceso o por defecto. Si es corto, dos workers pueden procesar el mismo trabajo. Si es largo, un worker caído deja el job invisible demasiado tiempo. Mide tiempos reales y divide trabajos largos.

Prompt para Claude Code

Un prompt útil describe el contrato de fallo:

Agrega una cola de envío de email. La API debe guardar la solicitud y encolar solo deliveryId y templateId. El worker debe usar idempotency key para evitar doble envío, reintentar errores temporales del proveedor hasta 3 veces con exponential backoff y mover fallos repetidos a una tabla DLQ. No guardes API keys, cuerpos de email ni datos personales en el payload. Expón queue depth, oldest job age, failure rate y DLQ count. Agrega tests para entrega duplicada, poison message y visibility timeout.

Con esa instrucción, Claude Code tiene criterios de diseño y pruebas. La diferencia se nota en review.

Documentación oficial y elección de herramienta

Si tu infraestructura está en AWS, empieza con Amazon SQS Developer Guide. Si necesitas routing, exchanges, pub/sub o una topología de mensajería más flexible, lee RabbitMQ documentation. Si tu stack Node.js ya usa Redis y quieres delayed jobs, repeatable jobs y buena ergonomía de workers, revisa BullMQ documentation.

No elijas por nombre de librería. Primero define payload, idempotencia, retry, DLQ, monitoreo, permisos, coste y experiencia operativa del equipo.

Fallos frecuentes

El primer fallo es duplicate processing. La cola puede entregar más de una vez por timeout, reinicio del worker o pérdida de ack. La solución no está en confiar en la cola, sino en proteger el efecto de negocio.

El segundo fallo es poison message. Un payload con schema viejo o usuario inexistente no se arregla con más retry. Debe validarse, registrarse y terminar en DLQ con replay controlado.

El tercero es infinite retry. Durante una caída del proveedor, reintentar inmediatamente aumenta el tráfico y retrasa la recuperación. Usa intentos finitos, backoff, jitter y backpressure.

El cuarto es guardar secrets en payloads. Las colas aparecen en logs, DLQ, dashboards y herramientas de soporte. Usa IDs de referencia y deja que el worker lea datos sensibles desde una fuente autorizada.

Formación y consultoría

Las colas parecen simples en código, pero difíciles en operación. ClaudeCodeLab puede ayudar a convertir esta checklist en proceso de equipo: prompts para Claude Code, reglas en CLAUDE.md, schema de payload, runbook de DLQ, métricas y revisión CI. Para equipos, usa formación y consultoría Claude Code. Para trabajo individual, pega esta checklist en tu plantilla de PR.

Resumen

Una cola de trabajos es infraestructura de producción. Controla trabajo lento, aísla fallos, evita efectos duplicados, limita concurrencia y deja evidencia operativa. Cuando pidas a Claude Code implementar una cola, incluye producer, consumer, payload, visibility timeout, retry, DLQ, idempotencia, backpressure y monitoreo desde el primer prompt.

Resultado probado por Masa: ejecuté localmente los tres ejemplos Node.js sin servicios externos y verifiqué el flujo básico de cola, la protección ante entrega duplicada y el envío de poison messages a DLQ. El ejemplo de idempotencia fue especialmente útil porque el segundo delivery del mismo email reutilizó el resultado guardado en vez de enviar otra vez.

#Claude Code #colas de trabajos #procesamiento asíncrono #BullMQ #Redis
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.