Kapan Pakai Queue, Kapan Tidak

Salah satu pertanyaan yang paling sering muncul saat saya review kode developer lain adalah: "ini job-nya perlu diantre nggak sih?" Jawabannya sering dianggap sepele -- kalau berat, queue-kan saja. Padahal keputusan ini punya konsekuensi nyata: queue menambah moving part baru (worker, koneksi antrian, monitoring), dan kalau job-nya tidak idempotent, retry otomatis dari queue bisa bikin masalah yang lebih parah daripada lambatnya request sinkron. Jadi menurut saya ini bukan soal "berat atau ringan", tapi soal beberapa pertanyaan yang lebih spesifik.

Kapan cukup dijalankan sinkron

Kalau hasil pekerjaan itu dibutuhkan user secara langsung untuk melanjutkan alurnya, dan durasinya bisa diprediksi tetap pendek, saya biasanya biarkan sinkron dulu. Contohnya validasi data, kalkulasi ringan, atau memanggil satu API eksternal yang cepat dan stabil. User submit form, dapat hasil, selesai -- tidak perlu polling status atau menunggu notifikasi terpisah.

Menambahkan queue di sini justru menambah kompleksitas tanpa manfaat: sekarang ada state "pending" yang harus ditangani di UI, ada kemungkinan worker mati sehingga job menumpuk tanpa disadari, dan debugging jadi lebih susah karena error tidak langsung terlihat di request yang sama. Sebagai ilustrasi, kalau proses tersebut biasanya selesai di bawah satu-dua detik dan gagalnya jarang, sinkron biasanya lebih sederhana dan lebih mudah dipelihara.

Kapan job harus diantre

Sebaliknya, saya pakai queue kalau salah satu dari kondisi ini terpenuhi: durasinya tidak bisa diprediksi (misalnya menunggu response dari layanan pihak ketiga yang kadang lambat), hasilnya tidak dibutuhkan user secara instan (kirim email, generate laporan, sinkronisasi data ke sistem lain), atau prosesnya cukup berat sehingga memblokir worker web/PHP-FPM dalam waktu lama bisa mengganggu request lain yang sedang berjalan bersamaan.

Contoh konkret yang sering saya temui di sistem klinik: generate rekam medis dalam format PDF, kirim notifikasi ke banyak channel sekaligus, atau memanggil layanan AI yang butuh beberapa detik untuk merespons. Semua ini cocok diantre karena user tidak perlu menunggu di layar loading, dan kalau layanan eksternalnya sedang bermasalah, kegagalannya tidak langsung merusak pengalaman utama aplikasi.

Jebakan retry yang tidak idempotent

Bagian yang paling sering diabaikan justru bukan soal kapan pakai queue, tapi apa yang terjadi kalau job itu di-retry. Queue worker pada dasarnya mengasumsikan job bisa gagal dan dicoba ulang -- entah karena koneksi terputus, exception sementara, atau worker mati di tengah proses. Kalau job itu tidak idempotent, retry ini bisa berbahaya.

Contoh klasik: job yang mengirim notifikasi lalu menambah saldo poin user. Kalau job ini gagal setelah notifikasi terkirim tapi sebelum status job ditandai selesai, worker akan mencobanya lagi dari awal -- user bisa dapat notifikasi dua kali dan saldo poin bertambah dua kali. Job semacam ini harus dibuat idempotent, misalnya dengan mengecek dulu apakah aksi itu sudah pernah dijalankan (pakai penanda unik per job atau per transaksi) sebelum benar-benar mengeksekusi efek sampingnya.

if (Payment::where('reference', $this->reference)->exists()) {
    return; // sudah diproses, jangan ulangi efek sampingnya
}

Prinsip yang saya pegang: kalau saya tidak yakin job itu aman dijalankan dua kali, saya anggap dia belum siap masuk queue, apa pun beratnya proses itu.

Takeaway

Keputusan queue atau sinkron sebaiknya dimulai dari pertanyaan tentang kebutuhan user dan prediktabilitas durasi, bukan sekadar "berat berarti queue". Dan begitu sebuah job masuk queue, anggap dia pasti akan di-retry suatu saat -- jadi pastikan efek sampingnya aman dijalankan berulang kali sebelum kode itu naik ke produksi.