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.
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érmino | Explicación simple | Decisión de diseño |
|---|---|---|
| Producer | Código que encola trabajos | Forma del payload, validación, prioridad, clave de deduplicación |
| Consumer | Worker que toma y ejecuta trabajos | Concurrencia, timeout, política de fallo |
| Message payload | Datos que lee el worker | IDs, tipo, versión de schema, sin secretos |
| Visibility timeout | Tiempo durante el cual un job tomado no se entrega a otro worker | Un poco mayor que el p95 de procesamiento |
| Retry | Reejecución ante fallos temporales | Máximo de intentos, backoff, jitter, razón del fallo |
| DLQ | Cola para trabajos que ya no deben reintentarse automáticamente | Dueño, alerta, reglas de replay |
| Idempotencia | Repetir un job sin duplicar el resultado de negocio | Clave única, tabla de procesados, clave del proveedor |
| Backpressure | Frenar entrada cuando falta capacidad | Límite de concurrencia, rate limit, umbral de profundidad |
| Monitoreo | Evidencia de salud de la cola | Profundidad, 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
deliveryIdytemplateId. 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.
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.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Permission receipt para Claude Code: alcance, prueba y rollback
Patrón de permission receipt para Claude Code: acciones permitidas, aprobación, pruebas, rollback y CTA de ingresos.
Agent Harness seguro para Claude Code y Codex: permisos, verificacion y rollback
Diseña un Agent Harness seguro para Claude Code y Codex con permisos, plan, verificaciones y rollback.
Subagentes de Claude Code: guía práctica para delegar trabajo de forma segura
Guía práctica de subagentes en Claude Code para dividir artículos y código: reglas, prompts, riesgos y checklist.