Advanced (Mis à jour: 02/06/2026)

Système de files d'attente avec Claude Code : guide pratique asynchrone

Concevoir producers, workers, retries, DLQ, idempotence et monitoring avec Claude Code.

Système de files d'attente avec Claude Code : guide pratique asynchrone

Quand Claude Code aide à construire une application web, on peut facilement tout mettre dans le handler HTTP. Le formulaire envoie l’email avant de répondre, l’upload d’image crée les miniatures dans la même requête, et le webhook de paiement met à jour la commande, envoie le reçu et écrit dans le CRM. En démonstration, c’est simple. En production, on ajoute les timeouts, les doublons, les redémarrages de déploiement, les limites de fournisseurs et les échecs partiels.

Une file d’attente sépare la requête utilisateur du travail lent ou fragile. Le producer publie un job, le consumer ou worker le prend, lit le message payload, exécute l’effet de bord, confirme le succès, relance les erreurs temporaires et place les échecs répétés dans une dead-letter queue, ou DLQ. Il faut aussi définir le visibility timeout, c’est-à-dire la durée pendant laquelle un job pris par un worker reste caché aux autres workers ; l’idempotence, pour éviter un double effet métier si le même job revient ; le backpressure, pour ralentir l’entrée quand les workers ne suivent plus ; et le monitoring, pour voir si la file se bloque.

Les exemples ci-dessous sont des scripts Node.js sans dépendance. Ils ne demandent ni Redis, ni AWS, ni RabbitMQ. Ils servent à comprendre le contrat opérationnel avant de choisir SQS, RabbitMQ, BullMQ ou un autre broker.

Vue d’ensemble

Une file ne sert pas seulement à “faire en arrière-plan”. Elle découple les systèmes, protège les services externes, contrôle la concurrence, conserve les erreurs et donne aux opérateurs des preuves concrètes.

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"]
TermeSens simpleDécision à prendre
ProducerCode qui met un travail dans la fileFormat du payload, validation, priorité, clé de déduplication
ConsumerWorker qui prend et exécute un travailConcurrence, timeout, gestion d’échec
Message payloadDonnées lues par le workerIDs, type, version de schéma, pas de secrets
Visibility timeoutDurée où le job pris reste invisibleUn peu plus long que le p95 de traitement
RetryRelance d’une erreur temporaireNombre maximal, backoff, jitter, raison d’échec
DLQFile pour les jobs qui ne doivent plus être relancés automatiquementResponsable, alerte, règles de replay
IdempotenceRépéter le job sans dupliquer le résultat métierClé unique, table de jobs traités
BackpressureRalentir l’entrée quand la capacité manqueLimite de concurrence, rate limit, seuil de profondeur
MonitoringPreuve que la file est saine ou bloquéeProfondeur, plus vieux job, taux d’échec, nombre DLQ

Inclure ce vocabulaire dans le prompt Claude Code change la qualité du résultat. On obtient une implémentation relisible, pas seulement un exemple heureux.

Cas d’utilisation

Le premier cas est l’envoi d’emails. Emails de bienvenue, réinitialisation de mot de passe, relances de facture et réponses support ne devraient pas bloquer la réponse HTTP. Pour les détails, consultez l’automatisation email et l’implémentation SendGrid. Le payload doit contenir deliveryId, templateId et userId, pas une clé API ni le corps complet de l’email.

Le deuxième cas est le traitement d’images ou de vidéos. Miniatures, conversion WebP, scan antivirus, sous-titres ou previews peuvent consommer beaucoup de CPU. La file permet de répondre vite et de limiter le nombre de workers actifs. Le piège principal est la concurrence sans borne.

Le troisième cas est le retry de facturation. Un fournisseur de paiement ou un réseau de carte peut échouer temporairement. Une file de retry aide, mais elle doit être finie. Sans idempotence, backoff et DLQ, on risque les doubles paiements ou une surcharge du fournisseur.

Le quatrième cas est l’enrichissement de leads et la génération de rapports. Après un formulaire, on peut enrichir les données d’entreprise, écrire dans le CRM, générer un rapport commercial et notifier Slack. Ce flux s’articule avec l’architecture orientée événements, la journalisation et le monitoring et les bonnes pratiques de sécurité.

Exemple 1 : file en mémoire sans dépendance

Enregistrez ce fichier sous queue-basic-demo.mjs, puis lancez node queue-basic-demo.mjs. Il montre producer, consumer, payload, visibility timeout et backpressure. Ce n’est pas un broker de production, car tout vit en mémoire, mais le cycle de vie est clair.

// 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 production, le tableau ready sera remplacé par SQS, RabbitMQ, Redis ou un service équivalent. Le modèle reste identique : prêt, en cours, confirmé ou remis en file après expiration.

Exemple 2 : garde d’idempotence

La plupart des files fonctionnent en at-least-once delivery. Le même job peut donc revenir. Sans garde, le worker peut envoyer deux emails, facturer deux fois ou créer deux lignes 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 production, remplacez Map par une contrainte unique en base, Redis SETNX ou une clé d’idempotence du fournisseur. Demandez à Claude Code de marquer le job terminé seulement après l’effet externe, de libérer le lock en cas d’erreur et de ne jamais stocker les secrets dans le payload.

Exemple 3 : retry et DLQ

Le retry corrige les erreurs temporaires. Il ne corrige pas un payload invalide, un utilisateur supprimé, un droit manquant ou une configuration absente. Un poison message doit sortir de la file principale.

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

Une DLQ doit avoir un propriétaire. Il faut une alerte, la raison de l’échec, une procédure de correction et une règle de replay. Sinon, elle devient une perte de données silencieuse.

Checklist opérationnelle

  • Le payload contient jobId, type, schemaVersion, un ID métier et une clé d’idempotence.
  • Le payload ne contient pas de clé API, token OAuth, données de carte, corps complet d’email ou texte personnel long.
  • Le producer valide avant d’enfiler le job.
  • Le visibility timeout dépasse légèrement le p95 de traitement.
  • Retry, backoff, jitter et DLQ sont définis avant la production.
  • La concurrence du worker respecte DB, rate limit, CPU et mémoire.
  • On surveille profondeur, plus vieux job, actifs, taux d’échec, DLQ et p95.
  • Un runbook explique revue, correction, replay et suppression de la DLQ.
  • Email, facturation, points et CRM sont conçus pour des livraisons dupliquées.
  • La review Claude Code couvre les erreurs, pas seulement le chemin heureux.

Le visibility timeout est un réglage discret mais critique. Trop court, il crée du double traitement. Trop long, il cache les jobs après un crash de worker. Mesurez le p95 réel et découpez les travaux longs.

Prompt pour Claude Code

Donnez le contrat d’échec, pas seulement le nom d’une librairie :

Ajoute une file d’envoi d’email. L’API sauvegarde la demande puis enfile seulement deliveryId et templateId. Le worker utilise une clé d’idempotence pour éviter les doubles envois, relance les erreurs temporaires du provider au maximum 3 fois avec exponential backoff, puis place les échecs répétés dans une table DLQ. Ne mets pas de clés API, corps d’email ou données personnelles dans le payload. Expose queue depth, oldest job age, failure rate et DLQ count. Ajoute des tests pour livraison dupliquée, poison message et visibility timeout.

Avec cette demande, Claude Code produit plus souvent une base que l’on peut relire et opérer.

Documentation officielle

Si votre infrastructure est centrée sur AWS, commencez par Amazon SQS Developer Guide. Pour des topologies de messaging, exchanges, routing ou pub/sub plus fins, lisez RabbitMQ documentation. Pour un service Node.js déjà basé sur Redis avec delayed jobs et repeatable jobs, consultez BullMQ documentation.

Ne choisissez pas l’outil avant le contrat. Payload, idempotence, retry, DLQ, monitoring, permissions, coût et expérience de l’équipe doivent guider le choix.

Pièges fréquents

Le premier piège est le duplicate processing. Une file promet souvent une livraison au moins une fois, pas un effet métier exactement une fois. Le garde-fou doit être au point où l’effet métier se produit.

Le deuxième piège est le poison message. Un schema obsolète ou un utilisateur supprimé ne sera pas corrigé par dix retries. Il faut valider, enregistrer la raison, isoler en DLQ et rejouer seulement après correction.

Le troisième piège est la boucle de retry infinie. Lors d’une panne provider, les retries immédiats augmentent la charge. Utilisez un nombre fini, backoff, jitter et backpressure côté producer.

Le quatrième piège est de mettre des secrets dans le payload. Les files apparaissent dans les logs, DLQ, dashboards et outils de support. Le payload doit contenir des IDs de référence, et le worker doit relire les données sensibles depuis une source autorisée.

Formation et conseil

Les files sont simples à coder mais difficiles à opérer. ClaudeCodeLab peut transformer cette checklist en processus d’équipe : prompts Claude Code, règles CLAUDE.md, schémas de payload, runbook DLQ, métriques et review CI. Pour une équipe, utilisez la formation et le conseil Claude Code. Pour un travail individuel, copiez la checklist dans votre modèle de pull request.

Résumé

Une file de jobs est une infrastructure de production. Elle contrôle le travail lent, isole l’échec, évite les effets doublés, limite la concurrence et laisse des preuves d’exploitation. Quand vous demandez à Claude Code d’implémenter une file, spécifiez producer, consumer, payload, visibility timeout, retry, DLQ, idempotence, backpressure et monitoring dès le premier prompt.

Résultat vérifié par Masa : j’ai exécuté localement les trois exemples Node.js sans service externe et confirmé le flux de base, la protection contre la livraison dupliquée et le passage des poison messages en DLQ. L’exemple d’idempotence est particulièrement utile dans un prompt, car la deuxième livraison du même email réutilise le résultat enregistré au lieu d’envoyer une deuxième fois.

#Claude Code #files d'attente #traitement asynchrone #BullMQ #Redis
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.