Laravel Queue — Cara Kerja, Driver, dan Kapan Harus Dipakai

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

DatabaseRedisSQS
SetupSangat mudahPerlu Redis serverPerlu akun AWS
KecepatanLambatPaling cepatSedang
ReliabilitySedangButuh config AOF/RDBPaling reliable
ScalabilityTerbatasBaikAuto-scale
BiayaGratisServer RedisPer request
Cocok untukDev / kecilProduction umumCloud-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.