Memisahkan worker dari API adalah keputusan arsitektur untuk memindahkan pekerjaan tertentu dari jalur request-response ke proses background berbasis queue. Pendekatan ini berguna ketika pekerjaan terlalu lama, rentan gagal sementara, perlu retry, atau dapat diproses secara asinkron tanpa menahan respons API.

Namun, tidak semua proses berat otomatis harus dipindah ke worker. Jika hasilnya wajib tersedia saat itu juga, konsistensi harus kuat, atau overhead queue lebih besar daripada manfaatnya, proses inline di request API bisa tetap lebih tepat. Keputusan yang baik harus berbasis metrik, karakteristik bisnis, dan kesiapan operasi sistem.

Kapan proses tetap inline di request API?

Proses inline berarti API mengeksekusi pekerjaan sampai selesai sebelum mengirim respons ke client. Ini cocok ketika client benar-benar membutuhkan hasil final secara langsung dan durasi proses masih berada dalam batas latensi yang dapat diterima.

Sinyal bahwa inline masih layak

  • Durasi stabil dan pendek: p95 dan p99 masih berada dalam latency budget endpoint. Jangan hanya melihat rata-rata karena outlier sering menjadi sumber timeout.
  • Hasil dibutuhkan sinkron: misalnya validasi kupon, kalkulasi ongkir, pengecekan saldo, atau otorisasi pembayaran.
  • Operasi harus langsung menentukan respons: API tidak bisa menjawab sukses sebelum operasi benar-benar selesai.
  • Throughput masih aman: penambahan traffic belum menyebabkan thread, connection pool, CPU, atau memory jenuh.
  • Kegagalan harus langsung terlihat ke user: client perlu tahu saat itu juga apakah operasi berhasil atau gagal.

Contoh: endpoint checkout perlu memvalidasi stok, alamat, harga, dan metode pembayaran sebelum membuat order. Beberapa langkah ini umumnya tetap inline karena memengaruhi keputusan respons. Jika dipindahkan ke worker tanpa desain yang benar, user bisa menerima status ambigu atau order dibuat dalam kondisi tidak valid.

Kelemahan proses inline

  • Request lebih mudah timeout ketika dependency eksternal lambat.
  • Kapasitas API cepat habis karena worker thread atau event loop menunggu pekerjaan panjang.
  • Retry dari client bisa berbahaya jika endpoint tidak idempotent, misalnya membuat transaksi ganda.
  • Tail latency meningkat: p99 yang buruk sering terasa sebagai sistem “kadang lambat” walaupun rata-rata terlihat baik.

Kapan dipindahkan ke worker berbasis queue?

Worker berbasis queue cocok untuk pekerjaan yang tidak harus selesai sebelum API menjawab, dapat diproses ulang dengan aman, dan membutuhkan kontrol kapasitas terpisah dari API. API cukup menerima permintaan, menyimpan state awal, memasukkan job ke queue, lalu mengembalikan respons seperti 202 Accepted atau status “queued”.

Sinyal kuat untuk memakai worker

  • p95/p99 endpoint memburuk karena proses berat: terutama jika waktu habis di operasi CPU-intensive, generate file, upload besar, atau panggilan pihak ketiga.
  • Durasi job tidak konsisten: sebagian selesai cepat, sebagian lain bisa berlangsung puluhan detik atau menit.
  • Butuh retry terkontrol: misalnya API vendor kadang gagal dengan timeout atau rate limit.
  • Butuh backpressure: sistem harus bisa menahan lonjakan pekerjaan tanpa menjatuhkan API utama.
  • Throughput perlu diskalakan terpisah: API dan worker punya pola resource berbeda. API mungkin IO-bound, sedangkan worker bisa CPU-bound.
  • Hasil dapat diambil belakangan: client bisa polling status, menerima webhook, notifikasi, atau membuka halaman hasil setelah selesai.

Dengan queue, API tidak lagi menunggu pekerjaan panjang. Worker mengonsumsi job sesuai kapasitas. Jika beban naik, queue length bertambah dan dapat dipantau. Ini bukan menghilangkan pekerjaan, tetapi memindahkan tekanan dari jalur request ke jalur background yang lebih mudah dikontrol.

Catatan: queue bukan solusi ajaib untuk performa. Jika worker lebih lambat daripada laju job masuk, backlog tetap akan tumbuh. Bedanya, backlog tersebut terlihat, dapat diberi limit, dan tidak langsung membuat semua request API timeout.

Sinyal keputusan yang perlu diukur

1. Latensi p95 dan p99

Gunakan p95 dan p99 untuk memahami pengalaman request terburuk yang masih sering terjadi. Rata-rata bisa menipu. Endpoint dengan rata-rata 300 ms tetapi p99 beberapa detik tetap bermasalah untuk user dan client automation.

Jika bagian yang memperburuk p99 bukan bagian yang wajib untuk respons, itu kandidat kuat untuk dipindahkan ke worker. Contohnya mengirim email setelah registrasi, membuat PDF laporan, atau sinkronisasi data ke CRM.

2. Durasi dan variasi job

Pekerjaan berdurasi panjang dan bervariasi lebih cocok untuk worker. Generate laporan bisa cepat untuk 100 baris, tetapi sangat lambat untuk jutaan baris. Jika tetap inline, endpoint harus menanggung variasi tersebut dan client harus menunggu tanpa kepastian.

Untuk job panjang, desain API sebaiknya mengembalikan ID pekerjaan:

POST /reports
Response: 202 Accepted
{
  "report_id": "rpt_123",
  "status": "queued"
}

GET /reports/rpt_123
Response: 200 OK
{
  "status": "processing",
  "progress": 40
}

3. Retry dan idempotency

Queue hampir selalu berarti ada kemungkinan retry. Retry dapat terjadi karena worker crash, timeout, koneksi database putus, atau dependency eksternal gagal. Karena itu, handler job harus idempotent: job yang sama boleh dijalankan lebih dari sekali tanpa menghasilkan efek samping ganda.

Contoh praktik idempotency:

  • Gunakan idempotency key untuk operasi yang membuat resource atau transaksi.
  • Simpan status job: queued, processing, succeeded, failed.
  • Buat constraint unik pada operasi yang tidak boleh duplikat, misalnya payment_id, invoice_number, atau external_reference.
  • Pastikan integrasi pihak ketiga mendukung deduplikasi, atau simpan mapping request internal ke response eksternal.
async function handleSendEmailJob(job) {
  const alreadySent = await emailLog.exists({ idempotencyKey: job.idempotencyKey });
  if (alreadySent) return;

  await emailProvider.send({
    to: job.to,
    subject: job.subject,
    body: job.body
  });

  await emailLog.insert({
    idempotencyKey: job.idempotencyKey,
    sentAt: new Date()
  });
}

Contoh di atas menunjukkan prinsipnya: sebelum melakukan efek samping, worker memeriksa apakah efek tersebut sudah pernah dilakukan. Dalam sistem produksi, perhatikan race condition. Deduplication sebaiknya diperkuat dengan constraint unik atau operasi insert atomik.

4. Throughput dan backpressure

Throughput adalah laju pekerjaan yang bisa diselesaikan sistem. Jika API menerima 1.000 job per menit tetapi worker hanya mampu menyelesaikan 300 job per menit, backlog akan terus tumbuh. Queue membantu menyerap lonjakan sementara, tetapi bukan pengganti kapasitas yang cukup.

Backpressure diperlukan agar sistem tidak menerima pekerjaan tanpa batas. Bentuknya bisa berupa:

  • membatasi jumlah job aktif per tenant atau per user;
  • menolak request baru saat queue terlalu panjang;
  • mengembalikan respons retry-later;
  • menurunkan prioritas job non-kritis;
  • memisahkan queue berdasarkan prioritas, misalnya critical, default, dan bulk.

5. Kebutuhan konsistensi

Ini sering menjadi faktor penentu. Worker memperkenalkan eventual consistency: API bisa menjawab sebelum pekerjaan selesai. Jika domain tidak bisa menerima state sementara, proses mungkin harus tetap inline atau menggunakan desain transaksi yang lebih ketat.

Contoh: pengiriman email konfirmasi bisa eventual consistent. Namun, mengurangi saldo pengguna tidak boleh “nanti saja” tanpa kontrol kuat. Untuk pembayaran, bagian otorisasi dan pencatatan status awal biasanya harus sinkron dan konsisten, sedangkan notifikasi, rekonsiliasi, atau sinkronisasi analitik dapat berjalan di worker.

Contoh skenario umum

Email dan notifikasi

Pengiriman email hampir selalu cocok untuk worker. User tidak perlu menunggu SMTP atau provider email selesai. API cukup mencatat event dan mengantrekan job. Jika provider lambat, worker bisa retry tanpa mengganggu endpoint registrasi atau checkout.

Yang perlu diperhatikan: jangan mengirim email dua kali akibat retry. Gunakan idempotency key seperti welcome-email:user-id atau invoice-email:invoice-id.

Generate laporan

Generate laporan besar sebaiknya dipindah ke worker karena durasinya bisa sangat bervariasi dan sering melibatkan query berat, pemrosesan file, serta upload ke object storage. API dapat membuat record laporan dengan status queued, lalu worker menghasilkan file dan memperbarui status menjadi ready.

Untuk laporan yang kecil dan interaktif, inline masih masuk akal jika p95/p99 stabil dan tidak membebani database. Pisahkan laporan real-time kecil dari laporan ekspor besar.

Sinkronisasi pihak ketiga

Integrasi ke pihak ketiga cocok untuk worker jika tidak dibutuhkan langsung untuk respons. API vendor bisa lambat, terkena rate limit, atau mengalami downtime. Worker memungkinkan retry dengan jeda, dead-letter queue, dan pemantauan kegagalan.

Namun, jika respons pihak ketiga menentukan keputusan bisnis saat itu juga, misalnya verifikasi identitas sebelum membuka akun, proses mungkin harus tetap inline dengan timeout ketat dan fallback state yang jelas.

Pembayaran

Pembayaran perlu dibagi hati-hati. Langkah yang menentukan apakah transaksi diterima, ditolak, atau menunggu konfirmasi biasanya tidak boleh diperlakukan seperti job background biasa. Endpoint harus menyimpan status transaksi secara konsisten dan menggunakan idempotency key untuk mencegah charge ganda.

Bagian yang sering cocok untuk worker adalah pengiriman receipt, update loyalty point, sinkronisasi ke sistem akuntansi, rekonsiliasi, dan notifikasi internal. Jangan mengantrekan “charge kartu kredit” tanpa mekanisme idempotency dan state machine yang kuat.

Pola implementasi yang lebih aman

Gunakan state eksplisit, bukan asumsi

Setiap pekerjaan asinkron sebaiknya memiliki state yang dapat dilihat dan diaudit. Minimal simpan status, waktu dibuat, waktu diproses, jumlah percobaan, error terakhir, dan referensi ke resource bisnis.

jobs
- id
- type
- payload
- idempotency_key
- status
- attempts
- last_error
- available_at
- created_at
- updated_at

Dengan state eksplisit, debugging menjadi lebih mudah. Tim dapat menjawab: job mana yang gagal, sejak kapan, berapa kali dicoba, dan apakah aman untuk dijalankan ulang.

Pertimbangkan outbox pattern

Masalah umum terjadi ketika API berhasil menulis data bisnis ke database, tetapi gagal memasukkan job ke queue. Akibatnya, data sudah berubah tetapi pekerjaan background tidak pernah jalan. Outbox pattern mengurangi risiko ini dengan menyimpan event/job dalam transaksi database yang sama dengan perubahan data bisnis. Proses terpisah kemudian menerbitkan event dari tabel outbox ke queue.

async function requestReport(userId, params) {
  return db.transaction(async (tx) => {
    const report = await tx.reports.insert({
      userId,
      params,
      status: 'queued'
    });

    await tx.outbox.insert({
      eventType: 'report.requested',
      aggregateId: report.id,
      payload: { reportId: report.id, userId },
      status: 'pending'
    });

    return report;
  });
}

Pola ini membuat perubahan data dan pencatatan event terjadi atomik di database yang sama. Setelah transaksi commit, publisher dapat membaca outbox dan mengirimkannya ke queue. Jika publisher gagal, record outbox masih ada dan bisa dicoba lagi.

Batasi retry dan gunakan dead-letter queue

Retry tanpa batas dapat memperparah insiden. Job yang selalu gagal akan menghabiskan kapasitas worker dan menunda job lain. Gunakan batas percobaan, jeda antar retry, dan tempat khusus untuk job gagal permanen seperti dead-letter queue atau tabel failed jobs.

Bedakan error sementara dan permanen. Timeout, koneksi putus, atau rate limit biasanya bisa dicoba ulang. Payload tidak valid, user tidak ditemukan, atau aturan bisnis yang gagal umumnya tidak akan berhasil hanya dengan retry.

Trade-off biaya, operasi, dan maintainability

Biaya infrastruktur

Memisahkan worker dari API menambah komponen: broker queue, proses worker, storage untuk status job, monitoring, dan kadang scheduler. Biaya ini bisa berupa server tambahan, managed service, bandwidth, storage, atau waktu engineering.

Namun, pemisahan juga bisa menghemat biaya pada skala tertentu. API dapat diskalakan untuk traffic request, sementara worker diskalakan berdasarkan backlog. Job CPU-heavy tidak perlu mengganggu instance API yang seharusnya ringan dan responsif.

Kompleksitas observability

Dengan worker, satu request user bisa berubah menjadi rangkaian event asinkron. Logging biasa tidak cukup. Anda perlu korelasi antar komponen menggunakan request ID, job ID, trace ID, atau correlation ID.

Metrik yang sebaiknya dipantau:

  • queue depth atau jumlah job menunggu;
  • job age, yaitu umur job tertua yang belum diproses;
  • success rate dan failure rate per jenis job;
  • durasi eksekusi job p95/p99;
  • jumlah retry dan dead-letter;
  • lag per prioritas atau tenant.

Deployment dan kompatibilitas

API dan worker sering memakai kode yang sama tetapi berjalan sebagai proses berbeda. Ini menimbulkan risiko saat deployment: API versi baru bisa mengirim payload job yang belum dipahami worker versi lama, atau sebaliknya.

Praktik yang membantu:

  • buat payload job kompatibel ke belakang;
  • gunakan field eksplisit dan hindari payload yang bergantung pada struktur internal object sementara;
  • deploy worker sebelum API jika API akan mengirim jenis job baru;
  • jangan menghapus handler job lama sebelum semua job lama selesai atau dimigrasikan;
  • versikan jenis job jika format payload berubah signifikan.

Debugging lebih sulit

Bug inline biasanya terlihat langsung pada respons API. Bug worker bisa muncul beberapa menit kemudian, hanya untuk sebagian job, atau setelah retry. Karena itu, worker membutuhkan tooling operasional: pencarian job berdasarkan ID, kemampuan requeue yang aman, log terstruktur, dan dashboard status.

Hindari debugging dengan menjalankan ulang job secara manual tanpa memahami idempotency. Requeue dapat menyebabkan efek samping ganda jika handler tidak aman.

Maintainability

Worker yang awalnya sederhana bisa menjadi tempat pembuangan semua proses “nanti saja”. Jika tidak dijaga, logic bisnis tersebar antara controller API, job handler, scheduler, dan consumer event. Ini membuat perubahan domain sulit diuji.

Letakkan aturan bisnis inti pada service/domain layer yang dapat dipanggil baik dari API maupun worker. Job handler sebaiknya tipis: validasi payload, ambil data, panggil service, update status, dan catat hasil.

Panduan praktis pengambilan keputusan

Gunakan pertanyaan berikut sebelum memindahkan proses dari API ke worker:

  1. Apakah hasilnya wajib tersedia untuk respons API? Jika ya, pertahankan inline atau pecah proses menjadi bagian sinkron dan asinkron.
  2. Apakah p95/p99 endpoint melewati latency budget? Jika ya, identifikasi bagian yang bisa ditunda.
  3. Apakah durasi proses bervariasi besar? Jika ya, worker biasanya lebih aman.
  4. Apakah proses aman di-retry? Jika belum, desain idempotency sebelum memakai queue.
  5. Apakah sistem siap menerima eventual consistency? Jika user atau domain butuh status final langsung, jangan sembarang asinkron.
  6. Apakah ada strategi backpressure? Queue tanpa limit dapat menyembunyikan overload sampai terlambat.
  7. Apakah observability sudah cukup? Minimal harus bisa melihat backlog, failure, retry, dan durasi job.
  8. Apakah deployment API dan worker kompatibel? Pastikan perubahan payload tidak merusak job lama.
  9. Apakah biaya tambahan sepadan? Pertimbangkan broker, worker, monitoring, dan beban operasional.

Anti-pattern yang perlu dihindari

  • Memindahkan semua proses lambat ke queue tanpa memahami domain. Beberapa proses lambat justru perlu dioptimalkan, diberi indeks database, atau dibuat lebih efisien, bukan sekadar disembunyikan.
  • Menganggap enqueue berarti sukses bisnis. Job yang masuk queue belum tentu berhasil diproses. Respons API harus membedakan “diterima untuk diproses” dan “selesai”.
  • Tidak membuat handler idempotent. Ini penyebab umum email ganda, invoice ganda, sinkronisasi kacau, atau transaksi duplikat.
  • Retry tanpa batas. Job gagal permanen akan memenuhi worker dan memperpanjang backlog.
  • Tidak memantau umur job tertua. Queue depth saja tidak cukup; queue kecil pun bisa bermasalah jika job tertua tersangkut terlalu lama.
  • Mencampur job kritis dan bulk dalam queue yang sama. Generate laporan besar bisa menunda notifikasi pembayaran jika tidak ada prioritas atau pemisahan queue.
  • Mengirim payload terlalu besar. Simpan data besar di storage/database dan kirim referensinya melalui queue.
  • Mengabaikan kompatibilitas deployment. Worker lama yang tidak paham format job baru dapat menyebabkan kegagalan massal.

Kesimpulan

Memisahkan worker dari API adalah alat untuk mengelola latensi, throughput, retry, dan backpressure, bukan tujuan arsitektur itu sendiri. Proses tetap inline jika hasilnya wajib sinkron, durasinya stabil, dan konsistensinya kritis. Pindahkan ke worker jika pekerjaan panjang, dapat ditunda, aman di-retry, dan sistem siap dengan eventual consistency.

Keputusan terbaik biasanya bukan “semua inline” atau “semua async”, melainkan pemisahan yang jelas: jalur API menangani validasi dan state penting secara cepat, sementara worker menangani pekerjaan background yang dapat diproses terkontrol, terukur, dan aman untuk diulang.