Advanced (Diperbarui: 2/6/2026)

Membangun Sistem Queue dengan Claude Code: Panduan Async Processing

Desain producer, worker, retry, DLQ, idempotency, dan monitoring queue dengan Claude Code.

Membangun Sistem Queue dengan Claude Code: Panduan Async Processing

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"]
IstilahArti sederhanaKeputusan desain
ProducerKode yang memasukkan pekerjaan ke queueBentuk payload, validasi, prioritas, kunci dedupe
ConsumerWorker yang mengambil dan menjalankan jobConcurrency, timeout, cara menangani gagal
Message payloadData yang dibaca workerID, tipe, schema version, tanpa secrets
Visibility timeoutWaktu job yang diambil tidak terlihat worker lainSedikit lebih panjang dari p95 processing time
RetryMenjalankan ulang kegagalan sementaraMaksimal attempt, backoff, jitter, alasan gagal
DLQQueue untuk job yang tidak boleh retry otomatis lagiOwner, alert, aturan replay
IdempotencyJob berulang tidak menggandakan hasil bisnisUnique key, tabel processed job
BackpressureMengurangi input saat kapasitas kurangConcurrency limit, rate limit, ambang queue depth
MonitoringBukti queue sehat atau tersendatDepth, 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 deliveryId dan templateId. 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.

#Claude Code #job queue #async processing #BullMQ #Redis
Gratis

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.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.