Queue-Systeme mit Claude Code bauen: Praxisguide für asynchrone Verarbeitung
Producer, Worker, Retries, DLQ, Idempotenz und Monitoring für Claude Code Queues.
Wenn Claude Code beim Bau einer Webanwendung hilft, landet schnell zu viel Logik im HTTP-Request. Das Kontaktformular sendet die E-Mail vor der Antwort, der Upload-Endpunkt erzeugt direkt Thumbnails, und ein Billing-Webhook aktualisiert Bestellung, Rechnung, E-Mail und CRM in einem Pfad. Für eine Demo ist das bequem. In Produktion kommen Timeouts, doppelte Requests, Deploy-Restarts, Provider-Limits und Teilausfälle dazu.
Ein Queue-System trennt die sichtbare Anfrage von langsamer oder fragiler Arbeit. Der Producer legt einen Job in die Queue, der Consumer oder Worker nimmt ihn, liest den Message Payload, führt die Arbeit aus, bestätigt Erfolg, wiederholt temporäre Fehler und verschiebt wiederholte Fehler in eine Dead-Letter Queue, kurz DLQ. Zusätzlich brauchst du Visibility Timeout, also die Zeit, in der ein genommener Job für andere Worker unsichtbar bleibt; Idempotenz, damit ein erneut gelieferter Job keinen doppelten Geschäftseffekt erzeugt; Backpressure, damit die Eingangsrate sinkt, wenn Worker nicht mehr hinterherkommen; und Monitoring, damit Staus sichtbar werden.
Die Beispiele in diesem Artikel sind dependency-freie Node.js-Skripte. Du brauchst kein Redis, kein AWS und kein RabbitMQ, um sie zu kopieren und auszuführen. Sie sollen den operativen Vertrag erklären, bevor du SQS, RabbitMQ, BullMQ oder einen anderen Broker auswählst.
Systemüberblick
Eine Queue ist nicht nur ein Trick für Hintergrundarbeit. Sie entkoppelt Systeme, schützt externe Dienste, begrenzt Parallelität, isoliert Fehler und liefert Belege für Betrieb und Debugging.
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"]
| Begriff | Einfache Bedeutung | Designentscheidung |
|---|---|---|
| Producer | Code, der Arbeit einreiht | Payload-Form, Validierung, Priorität, Dedupe-Key |
| Consumer | Worker, der Jobs nimmt und ausführt | Concurrency, Timeout, Fehlerverhalten |
| Message payload | Daten, die der Worker liest | IDs, Typ, Schema-Version, keine Secrets |
| Visibility timeout | Zeitraum, in dem ein Job nach Annahme unsichtbar ist | Etwas länger als p95-Verarbeitungszeit |
| Retry | Wiederholen temporärer Fehler | Maximalversuche, Backoff, Jitter, Fehlergrund |
| DLQ | Queue für Jobs, die nicht weiter automatisch laufen sollen | Owner, Alert, Replay-Regeln |
| Idempotenz | Wiederholung ohne doppelten Geschäftseffekt | Eindeutiger Schlüssel, Processed-Job-Tabelle |
| Backpressure | Eingang bremsen, wenn Kapazität fehlt | Concurrency-Limit, Rate Limit, Queue-Tiefe |
| Monitoring | Nachweis, ob die Queue gesund oder blockiert ist | Tiefe, ältester Job, Fehlerrate, DLQ-Zahl |
Diese Tabelle gehört in den Claude-Code-Prompt. Sie zwingt die Implementierung, Fehlerpfade und Betrieb mitzudenken.
Typische Anwendungsfälle
Der erste Anwendungsfall ist E-Mail-Versand. Welcome-Mails, Passwort-Resets, Rechnungswarnungen und Support-Antworten sollten die HTTP-Antwort nicht blockieren. Details findest du in E-Mail-Automatisierung und SendGrid E-Mail-Implementierung. Der Payload sollte deliveryId, templateId und userId enthalten, aber keine API-Keys, Tokens oder vollständigen Nachrichtentexte.
Der zweite Anwendungsfall ist Bild- und Videoverarbeitung. Thumbnails, WebP-Konvertierung, Virenscan, Untertitel und Preview-Clips sind CPU-lastig. Eine Queue erlaubt schnelle Annahme und kontrollierte Worker-Concurrency. Das größte Risiko ist unbegrenzte Parallelität.
Der dritte Anwendungsfall ist Billing-Retry. Payment Provider und Kartennetzwerke können temporär ausfallen. Eine Retry-Queue hilft, muss aber endlich sein. Ohne Idempotenz, Backoff und DLQ drohen doppelte Abbuchungen, Rate-Limit-Probleme und verdeckte Schemafehler.
Der vierte Anwendungsfall ist Lead-Enrichment und Report-Erzeugung. Nach einem Formular kannst du Firmendaten anreichern, CRM schreiben, Vertriebsreports erstellen und Slack informieren. Das passt zu eventgetriebener Architektur, Logging und Monitoring und Security Best Practices.
Beispiel 1: In-Memory Queue ohne Dependencies
Speichere die Datei als queue-basic-demo.mjs und führe node queue-basic-demo.mjs aus. Das Skript zeigt Producer, Consumer, Payload, Visibility Timeout und Backpressure. Es ist keine Produktionsqueue, weil alles im Speicher liegt, aber der Lebenszyklus ist nachvollziehbar.
// 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();
In Produktion ersetzt SQS, RabbitMQ, Redis oder ein anderer Broker das Array ready. Das Modell bleibt: bereit, in Arbeit, bestätigt oder nach Timeout zurückgegeben.
Beispiel 2: Idempotenz-Guard im Worker
Viele Queues liefern mindestens einmal. Ein Job kann also erneut auftauchen. Ohne Idempotenz entstehen doppelte E-Mails, doppelte Abbuchungen, doppelte Punkte oder doppelte CRM-Einträge.
// 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();
In Produktion nutzt du statt Map eine eindeutige DB-Constraint, Redis SETNX oder eine Provider-Idempotenz. Claude Code sollte explizit angewiesen werden, erst nach erfolgreichem externem Effekt als fertig zu markieren, bei Fehlern den Lock zu lösen und keine Secrets in den Payload zu schreiben.
Beispiel 3: Retry und DLQ
Retry hilft bei temporären Netzwerkfehlern. Er hilft nicht bei ungültigem Payload, gelöschten Nutzern, fehlenden Rechten oder falscher Konfiguration. Ein Poison Message muss aus der Hauptqueue heraus.
// 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();
Eine DLQ braucht einen Owner. Alert, Fehlergrund, Korrekturweg, Replay-Regel und Löschkriterium gehören dazu. Sonst ist sie nur ein stiller Speicher für verlorene Arbeit.
Operative Checkliste
- Payload enthält
jobId,type,schemaVersion, Business-ID und Idempotency-Key. - Payload enthält keine API-Keys, OAuth-Tokens, Kartendaten, vollständige E-Mail-Texte oder lange personenbezogene Daten.
- Producer validiert vor dem Enqueue.
- Visibility Timeout liegt leicht über p95-Verarbeitungszeit.
- Retry-Anzahl, Backoff, Jitter und DLQ-Regel sind vor Produktion definiert.
- Worker-Concurrency respektiert DB-Verbindungen, Provider-Limits, CPU und Speicher.
- Queue-Tiefe, ältester Job, aktive Jobs, Fehlerrate, DLQ-Anzahl und p95 werden überwacht.
- Ein Runbook beschreibt Review, Korrektur, Replay und Löschung von DLQ-Jobs.
- E-Mail, Billing, Punkte und CRM werden für doppelte Lieferung entworfen.
- Claude-Code-Reviews prüfen Fehlerpfade und Logs, nicht nur Erfolg.
Das Visibility Timeout ist kritisch. Zu kurz erzeugt doppelte Verarbeitung. Zu lang versteckt Jobs nach Worker-Crashs. Messe echte p95-Zeiten und teile lange Jobs auf.
Prompt für Claude Code
Ein guter Prompt beschreibt den Fehlervertrag:
Füge eine E-Mail-Delivery-Queue hinzu. Die API speichert die Anfrage und enqueued nur
deliveryIdundtemplateId. Der Worker nutzt einen Idempotency-Key gegen doppelte Sendungen, retryt temporäre Provider-Fehler maximal 3-mal mit exponential backoff und verschiebt wiederholte Fehler in eine DLQ-Tabelle. Keine API-Keys, E-Mail-Texte oder personenbezogenen Daten im Payload. Stelle queue depth, oldest job age, failure rate und DLQ count als Logs oder Metrics bereit. Ergänze Tests für doppelte Lieferung, poison message und visibility timeout.
Damit bekommt Claude Code genug Rahmen, um prüfbaren Code statt einer Kurz-Demo zu erzeugen.
Offizielle Dokumentation
Für AWS-zentrierte Infrastruktur ist der Amazon SQS Developer Guide der erste Startpunkt. Für Routing, Exchanges, Pub/Sub und flexible Messaging-Topologien passt RabbitMQ documentation. Wenn dein Node.js-Stack Redis nutzt und delayed jobs, repeatable jobs und Worker-Ergonomie braucht, lies BullMQ documentation.
Wähle das Tool nach dem Vertrag. Payload, Idempotenz, Retry, DLQ, Monitoring, Berechtigungen, Kosten und Team-Erfahrung sind wichtiger als der Paketname.
Häufige Fehler
Der erste Fehler ist duplicate processing. Queues garantieren meist mindestens eine Lieferung, nicht genau einen Geschäftseffekt. Der Schutz muss dort sitzen, wo der Geschäftseffekt passiert.
Der zweite Fehler ist poison message. Ein altes Schema oder ein gelöschter Nutzer wird durch Retry nicht gültig. Validieren, Fehlergrund speichern, in DLQ verschieben und kontrolliert replayen.
Der dritte Fehler ist infinite retry. Während eines Provider-Ausfalls verstärken sofortige Retries den Druck. Nutze endliche Versuche, Backoff, Jitter und Backpressure.
Der vierte Fehler sind Secrets im Payload. Queue-Daten landen in Logs, DLQs, Dashboards und Support-Tools. Payloads sollten Referenz-IDs enthalten; der Worker liest sensible Daten aus autorisierten Quellen.
Training und Beratung
Queues sind im Code überschaubar, im Betrieb aber anspruchsvoll. ClaudeCodeLab kann Prompts für Claude Code, CLAUDE.md-Regeln, Payload-Schemas, DLQ-Runbooks, Metrics und CI-Review-Kriterien als Teamprozess aufsetzen. Für Teams eignet sich Claude Code Training und Beratung. Für Einzelarbeit kannst du diese Checkliste in dein Pull-Request-Template übernehmen.
Zusammenfassung
Eine Job Queue ist Produktionsinfrastruktur. Sie kontrolliert langsame Arbeit, isoliert Fehler, verhindert doppelte Geschäftseffekte, begrenzt Parallelität und liefert Untersuchungsdaten. Wenn Claude Code eine Queue implementieren soll, gehören Producer, Consumer, Payload, Visibility Timeout, Retry, DLQ, Idempotenz, Backpressure und Monitoring in den ersten Prompt.
Masa hat die Beispiele praktisch geprüft: Die drei Node.js-Skripte liefen lokal ohne externe Dienste und zeigten den Grundfluss, den Schutz gegen doppelte Lieferung und das Verschieben von Poison Messages in die DLQ. Besonders der Idempotenz-Guard eignet sich als Prompt-Baustein, weil die zweite Lieferung desselben E-Mail-Jobs das gespeicherte Ergebnis nutzt statt erneut zu senden.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude-Code-Permission-Receipt: Scope, Beweis und Rollback festhalten
Permission-Receipt für Claude Code: erlaubte Aktionen, Freigabegrenzen, Prüfbefehle, Rollback und Umsatz-CTA-Prüfung.
Sicheres Agent Harness fur Claude Code und Codex: Rechte, Prufung und Rollback
Ein praktisches Agent Harness fur Claude Code und Codex mit Policy, Plan, Verifikation und Recovery.
Claude Code Subagents: Praxisleitfaden für sichere Agent-Delegation
Claude Code Subagents praktisch nutzen: Artikel- und Codearbeit sicher aufteilen, Prompts einsetzen, Fehler vermeiden.