Pernah kamu klik tombol “Kirim Invoice” dan layarnya loading lama banget? Mungkin sampai 5-10 detik? Itu tanda ada pekerjaan berat yang sedang dikerjakan langsung di request yang sama — generate PDF, kirim email, mungkin sekaligus update beberapa tabel.
User menunggu. Dan selama user menunggu, dia tidak bisa ngapa-ngapain.
Laravel Queue hadir untuk memecah situasi itu. Bukan dengan membuat prosesnya jadi lebih cepat — tapi dengan memindahkan pekerjaan beratnya ke belakang layar, supaya user bisa langsung lanjut.
🤔 Masalah yang Diselesaikan
Tanpa Queue, alurnya terlihat seperti ini:
Request → Controller → Kirim email → Generate PDF → Response
↑
User menunggu di sini
Dengan Queue, alurnya jadi:
Request → Controller → Dispatch Job → Response ✅ (langsung)
↓
Worker (background)
↓
Kirim email, generate PDF — di sini, tanpa ganggu user
User dapat response cepat. Pekerjaan beratnya tetap berjalan — hanya saja sekarang di proses terpisah, di waktu yang berbeda, tanpa menghalangi siapapun.
🏗️ Tiga Komponen Utama
Sebelum masuk ke alur kerja, penting untuk tahu tiga komponen yang bekerja bersama di sistem Queue.
Job adalah unit pekerjaan yang ingin ditunda. Satu Job = satu tugas spesifik. Misalnya SendInvoiceEmail atau GenerateMonthlyReport. Job bukan tempat untuk logika bisnis yang kompleks — dia cukup tahu satu hal: apa yang harus dikerjakan.
Queue Driver adalah tempat antrian disimpan. Bisa database, Redis, atau Amazon SQS. Driver ini yang “memegang” job selama menunggu dieksekusi.
Queue Worker adalah proses yang berjalan di background, terus-menerus mengambil job dari driver dan menjalankannya. Tanpa worker yang aktif, job yang sudah di-dispatch tidak akan pernah dijalankan.
🪜 Alur Kerja dari Awal Sampai Akhir
1. Buat Job-nya
php artisan make:job SendInvoiceEmail
📁 app/Jobs/SendInvoiceEmail.php
<?php
namespace App\Jobs;
use App\Models\Order;
use App\Mail\InvoiceMail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;
class SendInvoiceEmail implements ShouldQueue
{
use Queueable;
// Simpan ID, bukan object — penjelasannya ada di bawah
public function __construct(
public int $orderId
) {}
public function handle(): void
{
// Fetch ulang di sini, bukan di constructor
$order = Order::find($this->orderId);
Mail::to($order->user->email)->send(new InvoiceMail($order));
}
}
📌 Perhatikan implements ShouldQueue — ini yang memberitahu Laravel bahwa class ini harus masuk antrian, bukan langsung dijalankan. Tanpa interface ini, job langsung dieksekusi saat dispatch, mengalahkan tujuan Queue itu sendiri.
2. Dispatch dari Controller
public function store(Request $request)
{
$order = Order::create($request->validated());
// Job didaftarkan ke antrian — TIDAK dijalankan di sini
SendInvoiceEmail::dispatch($order->id);
// Response langsung dikirim ke user
return response()->json(['message' => 'Order berhasil dibuat'], 201);
}
Saat dispatch() dipanggil, yang terjadi adalah:
- Job diserialisasi menjadi JSON
- JSON itu disimpan ke driver (database / Redis / SQS)
- Response langsung dikirim ke user — tidak menunggu email terkirim
3. Worker Mengambil dan Menjalankannya
php artisan queue:work
Worker berjalan dalam loop:
ambil job dari driver
↓
jalankan handle()
↓
sukses? → hapus job dari antrian
gagal? → retry atau pindah ke failed_jobs
↓
ambil job berikutnya...
⚙️ Fitur-Fitur Queue yang Wajib Tahu
Retry Otomatis — Kalau Job Gagal
Job bisa gagal. Koneksi email putus, API eksternal timeout, database sedang bermasalah. Queue punya mekanisme retry bawaan.
class SendInvoiceEmail implements ShouldQueue
{
// Maksimal 3 percobaan sebelum dianggap gagal permanen
public int $tries = 3;
// Tunggu 10 detik sebelum retry pertama, 20 detik sebelum kedua, dst.
public int $backoff = 10;
}
Atau langsung di command worker:
php artisan queue:work --tries=3
Kalau sudah 3 kali tetap gagal, job masuk ke tabel failed_jobs. Dari sana masih bisa dijalankan ulang secara manual:
php artisan queue:retry all
Delay — Jalankan Nanti, Bukan Sekarang
// Kirim email onboarding 24 jam setelah user daftar
SendOnboardingEmail::dispatch($user->id)
->delay(now()->addHours(24));
Job tidak langsung dieksekusi begitu di-dispatch — dia menunggu di antrian sampai waktunya tiba.
Priority Queue — Mana yang Didahulukan
Tidak semua job setara urgensinya. Reset password jelas lebih urgent dari laporan bulanan.
// Dispatch ke antrian berbeda berdasarkan urgensi
SendPasswordReset::dispatch($user->id)->onQueue('high');
GenerateMonthlyReport::dispatch()->onQueue('low');
Worker dikonfigurasi untuk mendahulukan antrian high:
php artisan queue:work --queue=high,default,low
Worker akan menguras semua job di high dulu sebelum menyentuh default, baru terakhir low.
⚠️ Jebakan Serialisasi — Yang Sering Bikin Bug
Ini salah satu hal paling penting yang sering diabaikan pemula.
Job bisa dijalankan di proses yang berbeda, bahkan di server yang berbeda. Artinya, object yang hidup di memory saat dispatch() tidak bisa dibawa ke sana. Object harus diserialisasi dulu (diubah jadi JSON), disimpan ke driver, lalu di-deserialisasi saat worker menjalankannya.
// ❌ SALAH — membawa object Auth::user() yang hidup di memory
public function __construct()
{
$this->user = Auth::user(); // object ini tidak bisa di-serialize dengan aman
}
// ✅ BENAR — simpan ID saja, fetch ulang di handle()
public function __construct(
public int $userId
) {}
public function handle(): void
{
$user = User::find($this->userId); // fetch fresh dari DB
}
📌 Kenapa ini penting? Karena data user di database saat job dijalankan bisa saja sudah berbeda dari saat job di-dispatch. Fetch ulang di handle() memastikan job selalu bekerja dengan data yang paling baru.
🛡️ Job Idempotent — Aman Dijalankan Berkali-kali
Queue punya fitur auto-retry. Artinya dalam kondisi tertentu, satu job bisa dijalankan lebih dari sekali — dan itu harus tetap menghasilkan hasil yang benar.
// ❌ Job non-idempotent — berbahaya kalau diretry
public function handle(): void
{
Stock::decrement('qty', 1); // kalau dijalankan 3x, qty berkurang 3
}
// ✅ Job idempotent — aman diretry berapa kalipun
public function handle(): void
{
// Cek dulu apakah job ini sudah pernah diproses
if (ProcessedJob::where('job_id', $this->job->getJobId())->exists()) {
return; // sudah diproses, tidak perlu ulang
}
Stock::decrement('qty', 1);
// Tandai sudah diproses
ProcessedJob::create(['job_id' => $this->job->getJobId()]);
}
Prinsipnya sederhana: dijalankan 1 kali atau 100 kali, hasilnya harus tetap sama. Ini yang disebut idempotent — dan semua job yang punya retry wajib didesain seperti ini.
Anti-pattern yang harus dihindari: “matikan saja retry-nya”, atau “kasih tau user jangan klik dua kali”. Itu bukan solusi — itu hanya berharap network selalu stabil dan user selalu sempurna. Program yang harus idempotent, bukan behavior yang tidak bisa kita kendalikan.
🚗 Driver Queue — Mana yang Harus Dipilih?
Driver adalah “gudang” tempat job disimpan sambil menunggu dieksekusi. Laravel punya beberapa pilihan dengan karakteristik yang sangat berbeda.
Database — Paling Simpel, Paling Lambat
Cara kerjanya: job disimpan di tabel jobs di database yang sama dengan aplikasi. Worker terus-menerus polling tabel itu untuk mencari job baru.
QUEUE_CONNECTION=database
# Buat tabel jobs dan failed_jobs
php artisan queue:table
php artisan migrate
Kelebihannya jelas — tidak perlu service tambahan. Langsung pakai database yang sudah ada. Setup lima menit, langsung jalan.
Kekurangannya: worker terus-menerus query database meskipun tidak ada job baru. Di traffic tinggi, ini menambah beban DB yang signifikan. Latensi-nya juga lebih tinggi dibanding Redis karena membaca dari disk, bukan dari memory.
Cocok untuk: development, staging, aplikasi kecil dengan traffic rendah, atau kalau budget tidak memungkinkan infrastruktur tambahan.
Redis — Cepat, Efisien, Standar Production
Cara kerjanya: job disimpan di memory Redis. Worker menggunakan teknik blocking pop — kalau antrian kosong, worker diam menunggu. Begitu ada job masuk, langsung diambil. Hampir real-time.
QUEUE_CONNECTION=redis
Kelebihannya: sangat cepat, efisien, dan sudah jadi standar de facto untuk production Laravel. Mayoritas aplikasi Laravel skala menengah ke atas pakai Redis untuk queue — apalagi kalau sudah pakai Redis untuk caching, tidak perlu tambah service baru.
Kekurangannya: butuh Redis server. Dan karena Redis menyimpan data di RAM, ada risiko data hilang kalau Redis crash — kecuali persistence diaktifkan.
# redis.conf — aktifkan AOF untuk persistence
appendonly yes
appendfsync everysec
📌 Salah kaprah yang sering muncul: “Redis selalu lebih aman karena lebih cepat.” Tidak tepat. Redis cepat, tapi data-nya volatile by default. AOF (Append Only File) atau RDB (Redis Database Snapshot) yang membuat Redis aman untuk production.
Cocok untuk: production, job dengan volume tinggi, aplikasi yang butuh respons cepat, atau sudah punya Redis untuk caching.
Amazon SQS — Managed, Skalabel, Tidak Perlu Urus Server
Cara kerjanya: job disimpan di layanan antrian milik AWS. Worker menarik job via API. Semuanya dikelola AWS — scaling, availability, redundancy.
QUEUE_CONNECTION=sqs
Kelebihannya: auto-scaling tanpa konfigurasi manual, highly available, dan tidak perlu memikirkan server antrian sama sekali. Cocok untuk arsitektur multi-service atau microservice yang sudah berjalan di ekosistem AWS.
Kekurangannya: latensi lebih tinggi dari Redis karena setiap operasi adalah HTTP request ke AWS. Ada biaya per request. Dan bergantung pada ketersediaan AWS di region yang dipakai.
📌 Salah kaprah yang sering muncul: “SQS paling cepat karena cloud.” Tidak. Redis lebih cepat. SQS lebih reliable dan scalable tanpa effort — itu nilai jualnya, bukan kecepatan raw-nya.
Cocok untuk: arsitektur cloud-native di AWS, sistem dengan traffic yang sangat tidak stabil (spike besar tiba-tiba), atau kalau tim tidak mau repot urus infra antrian sendiri.
Sync — Untuk Testing, Bukan Production
QUEUE_CONNECTION=sync
Dengan driver sync, job langsung dijalankan saat di-dispatch — tidak masuk antrian, tidak butuh worker. Ini bukan untuk production. Ini khusus untuk testing dan development lokal supaya tidak perlu menjalankan worker terpisah.
📊 Perbandingan Driver
| Database | Redis | SQS | |
|---|---|---|---|
| Setup | Sangat mudah | Perlu Redis server | Perlu akun AWS |
| Kecepatan | Lambat | Paling cepat | Sedang |
| Reliability | Sedang | Butuh config AOF/RDB | Paling reliable |
| Scalability | Terbatas | Baik | Auto-scale |
| Biaya | Gratis | Server Redis | Per request |
| Cocok untuk | Dev / kecil | Production umum | Cloud-native |
💡 Prinsipnya: Pilih driver berdasarkan bottleneck aplikasimu, bukan tren. Aplikasi kecil tidak butuh Redis. Aplikasi besar di AWS tidak harus SQS kalau Redis sudah mencukupi.
🧭 Kapan HARUS Pakai Queue, Kapan Tidak?
Pakai Queue untuk:
- Kirim email, notifikasi push, atau SMS
- Generate laporan, PDF, atau file ekspor
- Sinkronisasi ke API eksternal (payment gateway, ERP, dsb.)
- Upload dan proses file (resize gambar, transcode video)
- Audit logging yang berat
- Pekerjaan apapun yang butuh lebih dari ~500ms
Jangan pakai Queue untuk:
- Logic yang hasilnya harus langsung dipakai di response — misalnya cek stok sebelum konfirmasi order
- Validasi input — user harus tahu hasilnya seketika
- Transaksi database inti yang tidak boleh ditunda atau gagal diam-diam
- Authorization — kalau ditolak, user harus tahu sekarang
💡 Ingat!
Ada satu hal yang sering disalahpahami: Queue bukan async penuh.
Di level request, ya — user dapat response langsung tanpa menunggu job selesai. Tapi di level worker, job tetap dijalankan satu per satu secara synchronous. Satu worker, satu job dalam satu waktu.
Kalau volume job tinggi dan satu worker tidak cukup, solusinya bukan ganti driver — tapi tambah jumlah worker. Di production, worker biasanya dikelola oleh process manager seperti Supervisor, supaya worker otomatis restart kalau mati dan jumlahnya bisa dikonfigurasi sesuai kebutuhan traffic.
Queue memindahkan waktu eksekusi, bukan memindahkan tanggung jawab bisnis. Pekerjaan tetap harus selesai — hanya saja sekarang tidak harus di sini, dan tidak harus sekarang.