Membangun Sistem Queue dengan Claude Code: Panduan Async Processing
Desain producer, worker, retry, DLQ, idempotency, dan monitoring queue dengan Claude Code.
Saat memakai Claude Code untuk membuat aplikasi web, mudah sekali memasukkan semua pekerjaan ke dalam handler HTTP. Form kontak langsung mengirim email sebelum response, upload gambar langsung membuat thumbnail, dan webhook pembayaran sekaligus memperbarui order, mengirim invoice, serta menulis ke CRM. Untuk demo, pola ini terlihat cepat. Di produksi, timeout, request duplikat, restart deploy, rate limit provider, dan partial failure akan muncul.
Sistem queue memisahkan request yang dilihat user dari pekerjaan yang lambat atau rapuh. Producer memasukkan job ke queue. Consumer atau worker mengambil job, membaca message payload, menjalankan efek samping, mengakui sukses, mencoba ulang kegagalan sementara, dan mengirim kegagalan berulang ke dead-letter queue atau DLQ. Kamu juga harus menentukan visibility timeout, yaitu durasi job disembunyikan dari worker lain saat sedang diproses; idempotency, yaitu sifat yang membuat job duplikat tidak menghasilkan efek bisnis ganda; backpressure, yaitu cara memperlambat input saat worker tidak sanggup; dan monitoring, yaitu metrik untuk melihat queue sehat atau macet.
Contoh dalam artikel ini adalah script Node.js tanpa dependency. Tidak perlu Redis, AWS, atau RabbitMQ untuk menjalankannya. Tujuannya memahami kontrak operasional sebelum memilih SQS, RabbitMQ, BullMQ, atau broker lain.
Gambaran Sistem
Queue bukan sekadar “jalan di background”. Queue memisahkan sistem, melindungi layanan eksternal, membatasi concurrency, mengisolasi kegagalan, dan memberi bukti saat operasi harus menelusuri masalah.
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"]
| Istilah | Arti sederhana | Keputusan desain |
|---|---|---|
| Producer | Kode yang memasukkan pekerjaan ke queue | Bentuk payload, validasi, prioritas, kunci dedupe |
| Consumer | Worker yang mengambil dan menjalankan job | Concurrency, timeout, cara menangani gagal |
| Message payload | Data yang dibaca worker | ID, tipe, schema version, tanpa secrets |
| Visibility timeout | Waktu job yang diambil tidak terlihat worker lain | Sedikit lebih panjang dari p95 processing time |
| Retry | Menjalankan ulang kegagalan sementara | Maksimal attempt, backoff, jitter, alasan gagal |
| DLQ | Queue untuk job yang tidak boleh retry otomatis lagi | Owner, alert, aturan replay |
| Idempotency | Job berulang tidak menggandakan hasil bisnis | Unique key, tabel processed job |
| Backpressure | Mengurangi input saat kapasitas kurang | Concurrency limit, rate limit, ambang queue depth |
| Monitoring | Bukti queue sehat atau tersendat | Depth, oldest job age, failure rate, DLQ count |
Masukkan istilah ini ke prompt Claude Code. Dengan begitu hasilnya tidak berhenti di happy path.
Use Case Praktis
Use case pertama adalah email sending queue. Email welcome, reset password, reminder invoice, dan balasan support sebaiknya tidak memblokir response HTTP. Lanjutkan dengan email automation dan SendGrid email guide. Payload cukup berisi deliveryId, templateId, dan userId; jangan isi API key, token, atau body email penuh.
Use case kedua adalah pemrosesan gambar dan video. Thumbnail, konversi WebP, virus scan, subtitle, dan preview clip bisa berat untuk CPU. Queue membuat API cepat mengembalikan status “accepted” dan worker menjalankan pekerjaan dengan concurrency terbatas. Kesalahan umum adalah membiarkan worker berjalan tanpa batas.
Use case ketiga adalah retry billing. Payment provider, jaringan kartu, atau sistem invoice dapat gagal sementara. Retry queue membantu, tetapi harus terbatas. Tanpa idempotency, backoff, dan DLQ, sistem bisa membuat charge ganda atau menabrak rate limit provider.
Use case keempat adalah lead enrichment dan report generation. Setelah form terkirim, sistem dapat memperkaya data perusahaan, menulis ke CRM, membuat laporan sales, dan memberi tahu Slack secara async. Desainnya cocok dibaca bersama event-driven architecture, logging dan monitoring, serta security best practices.
Contoh 1: In-Memory Queue Tanpa Dependency
Simpan sebagai queue-basic-demo.mjs, lalu jalankan node queue-basic-demo.mjs. Script ini menunjukkan producer, consumer, payload, visibility timeout, dan backpressure. Ini bukan queue produksi karena semua data berada di memory, tetapi lifecycle-nya mudah dilihat.
// 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();
Di produksi, array ready diganti oleh SQS, RabbitMQ, Redis, atau broker persisten lain. Modelnya tetap sama: job siap, sedang diproses, sudah ack, atau kembali ke queue setelah timeout.
Contoh 2: Idempotency Guard untuk Worker
Sebagian besar queue memberi at-least-once delivery. Job yang sama bisa datang lagi. Tanpa idempotency, worker dapat mengirim email dua kali, menagih dua kali, memberi poin dua kali, atau membuat record CRM ganda.
// 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();
Di produksi, ganti Map dengan unique constraint database, Redis SETNX, atau idempotency key dari provider. Minta Claude Code menandai selesai hanya setelah efek eksternal berhasil, membuka lock saat gagal, dan tidak menyimpan secrets di payload.
Contoh 3: Retry dan DLQ
Retry berguna untuk error sementara. Retry tidak memperbaiki payload invalid, user yang sudah dihapus, permission salah, atau konfigurasi provider yang hilang. Poison message harus keluar dari main queue.
// 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 bukan tempat sampah. Harus ada owner, alert, alasan gagal, cara memperbaiki, aturan replay, dan kriteria penghapusan.
Checklist Operasional
- Payload berisi
jobId,type,schemaVersion, business ID, dan idempotency key. - Payload tidak berisi API key, OAuth token, data kartu, body email penuh, atau data pribadi panjang.
- Producer memvalidasi payload sebelum enqueue.
- Visibility timeout sedikit lebih panjang dari p95 processing time.
- Retry count, backoff, jitter, dan aturan DLQ sudah ditulis sebelum produksi.
- Worker concurrency mengikuti koneksi DB, rate limit provider, CPU, dan memory.
- Monitor queue depth, oldest job age, active count, failure rate, DLQ count, dan p95.
- Runbook menjelaskan review, replay, delete, dan komunikasi pelanggan untuk DLQ.
- Email, billing, poin, dan CRM didesain dengan asumsi duplicate delivery.
- Review Claude Code memeriksa failure path dan log, bukan hanya happy path.
Visibility timeout terlalu pendek membuat job yang sama diproses dua worker. Terlalu panjang membuat job tersembunyi lama saat worker mati. Ukur p95 nyata dan pecah job panjang menjadi langkah kecil.
Prompt untuk Claude Code
Prompt yang baik menjelaskan kontrak failure:
Tambahkan email delivery queue. API menyimpan request lalu enqueue hanya
deliveryIddantemplateId. Worker memakai idempotency key untuk mencegah pengiriman ganda, retry temporary provider error maksimal 3 kali dengan exponential backoff, lalu memindahkan kegagalan berulang ke tabel DLQ. Jangan simpan API key, body email, atau data pribadi di payload. Tampilkan queue depth, oldest job age, failure rate, dan DLQ count melalui log atau metrics. Tambahkan test untuk duplicate delivery, poison message, dan visibility timeout.
Dengan prompt seperti ini, Claude Code punya batasan produksi yang jelas.
Dokumentasi Resmi
Jika infrastruktur kamu AWS-first, mulai dari Amazon SQS Developer Guide. Jika butuh routing, exchange, pub/sub, dan topologi messaging yang fleksibel, baca RabbitMQ documentation. Jika stack Node.js sudah memakai Redis dan butuh delayed jobs, repeatable jobs, serta ergonomi worker, gunakan BullMQ documentation.
Jangan memilih tool dulu. Tentukan payload, idempotency, retry, DLQ, monitoring, permission, biaya, dan kemampuan operasi tim terlebih dahulu.
Kesalahan Umum
Duplicate processing adalah jebakan pertama. Queue biasanya menjamin minimal satu delivery, bukan satu efek bisnis. Ack hilang, worker restart, atau visibility timeout dapat mengirim job lagi.
Poison message adalah jebakan kedua. Schema rusak, user terhapus, atau permission salah tidak akan sembuh dengan retry. Validasi, simpan alasan, kirim ke DLQ, lalu replay setelah root cause diperbaiki.
Infinite retry adalah jebakan ketiga. Saat provider outage, retry langsung menambah traffic dan memperlambat recovery. Gunakan attempt terbatas, backoff, jitter, dan backpressure.
Secrets in payload adalah jebakan keempat. Data queue bisa muncul di log, DLQ, dashboard, dan tool support. Payload sebaiknya berisi reference ID, lalu worker membaca data sensitif dari store yang berwenang.
Training dan Konsultasi
Queue terlihat sederhana di kode, tetapi sulit di operasi. ClaudeCodeLab dapat membantu tim membuat prompt Claude Code, aturan CLAUDE.md, schema payload, runbook DLQ, metrics, dan review CI menjadi proses yang berulang. Untuk tim, gunakan Claude Code training and consulting. Untuk kerja individu, salin checklist ini ke template pull request.
Ringkasan
Job queue adalah infrastruktur produksi. Queue mengontrol pekerjaan lambat, mengisolasi failure, mencegah efek bisnis duplikat, membatasi concurrency, dan menyimpan bukti investigasi. Saat meminta Claude Code membuat queue, tulis producer, consumer, payload, visibility timeout, retry, DLQ, idempotency, backpressure, dan monitoring sejak prompt pertama.
Hasil uji langsung Masa: saya menjalankan tiga contoh Node.js ini secara lokal tanpa layanan eksternal dan memverifikasi alur queue dasar, perlindungan duplicate delivery, serta poison message yang masuk DLQ. Contoh idempotency paling berguna sebagai bahan prompt karena delivery kedua dari email yang sama memakai hasil tersimpan, bukan mengirim email lagi.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Permission receipt Claude Code: mencatat scope, bukti, dan rollback
Pola permission receipt untuk Claude Code: aksi yang diizinkan, batas approval, command verifikasi, rollback, dan cek CTA revenue.
Agent Harness Aman untuk Claude Code dan Codex: Permission, Verifikasi, dan Rollback
Rancang Agent Harness praktis untuk Claude Code dan Codex dengan policy, plan, verification, dan recovery layer.
Subagent Claude Code: panduan praktis untuk delegasi artikel dan kode
Panduan subagent Claude Code untuk membagi pekerjaan artikel dan kode: aturan delegasi, prompt, risiko, dan checklist.