Queue & Job — Laravel

Queue memproses tugas berat di background — user tidak perlu menunggu. Kapan pakai queue? Kirim email (lambat karena network) Process image (resize, compress)…

Queue (antrean) adalah daftar tugas yang dikerjakan nanti oleh proses lain, dan job adalah satu tugas di daftar itu. Bayangin dapur warung bakso: pelayan mencatat pesananmu di kertas, menjepitnya di rel tiket dapur, lalu langsung bilang "siap, ditunggu ya". Koki mengambil tiket satu per satu dari ujung rel. Pelayan gak ikut memasak, dan kamu gak perlu berdiri di depan kompor. Di Laravel, controller itu pelayannya, tabel jobs itu rel tiketnya, dan worker itu kokinya.

🧪 Tebakan cepat: server email mati. Job pengingat punya $tries = 3 dan $backoff = [10, 60]. Worker mencobanya pertama kali jam 09:00:00. Kapan job itu menyerah dan pindah ke tabel failed_jobs: 09:00:00, 09:01:10, atau gak pernah? Tebak dulu, jawabannya ada di bagian "Kalau job gagal".

Kenapa butuh antrean

Tiap pagi, app Perpustakaan mengirim email pengingat ke anggota yang bukunya jatuh tempo besok: Budi, Sari, dan Andi. Satu email butuh sekitar 800 ms karena harus bicara dengan server email. Kalau dikirim langsung di request, admin menunggu 2,4 detik, dan kalau server email sedang mati, request-nya gagal.

Dengan antrean, request cuma menulis 3 baris ke tabel jobs (sekitar 12 ms) lalu selesai. Pengirimannya dikerjakan worker di belakang. Kalau gagal, worker mencoba lagi. Konsepnya dibahas umum di lesson Message Queue (track System Design). Di sini cara Laravel-nya.

Job pertamamu

php artisan make:job SendDueReminder
<?php
// app/Jobs/SendDueReminder.php
namespace App\Jobs;

use App\Mail\DueReminder;
use App\Models\Loan;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Mail;

class SendDueReminder implements ShouldQueue
{
    use Queueable;

    public function __construct(public Loan $loan) {}

    public function handle(): void
    {
        Mail::to($this->loan->user)->send(new DueReminder($this->loan));
    }
}
// Di controller atau command: masukkan job ke antrean
SendDueReminder::dispatch($loan);

// Atau baru boleh diambil 5 menit lagi
SendDueReminder::dispatch($loan)->delay(now()->addMinutes(5));

Worker: php artisan queue:work

Job di tabel jobs gak jalan sendiri. Harus ada worker yang menyala:

$ php artisan queue:work

   INFO  Processing jobs from the [default] queue.

  2026-10-09 09:00:00 App\Jobs\SendDueReminder ....................... RUNNING
  2026-10-09 09:00:00 App\Jobs\SendDueReminder ................. 812.40ms DONE
  2026-10-09 09:00:00 App\Jobs\SendDueReminder ....................... RUNNING
  2026-10-09 09:00:01 App\Jobs\SendDueReminder ................. 790.15ms DONE
  2026-10-09 09:00:01 App\Jobs\SendDueReminder ....................... RUNNING
  2026-10-09 09:00:02 App\Jobs\SendDueReminder ................. 805.62ms DONE

Kalau job gagal: $tries, backoff, failed_jobs

Job yang melempar exception gak hilang. Worker mengembalikannya ke antrean untuk dicoba lagi, sebanyak yang kamu izinkan:

class SendDueReminder implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;           // paling banyak 3 percobaan
    public array $backoff = [10, 60]; // jeda sebelum percobaan ke-2 dan ke-3, dalam detik

    public function __construct(public Loan $loan) {}

    public function handle(): void { /* kirim email, seperti tadi */ }

    public function failed(?Throwable $exception): void
    {
        Log::warning("Pengingat loan #{$this->loan->id} gagal: {$exception?->getMessage()}");
    }
}

Dengan server email mati:

Percobaan Jam Hasil Lalu
1 09:00:00 FAIL kembali ke antrean, tunggu 10 detik
2 09:00:10 FAIL kembali ke antrean, tunggu 60 detik
3 09:01:10 FAIL $tries habis: failed() dipanggil, job pindah ke failed_jobs

Jadi job menyerah jam 09:01:10. Itu jawaban tebakannya.

Job harus aman diulang

Satu job bisa jalan lebih dari sekali: dicoba ulang setelah gagal di tengah jalan, atau worker mati (deploy, server restart) setelah handle() selesai tapi sebelum job-nya dihapus. Job yang tertahan begitu diambil lagi setelah retry_after, 90 detik di config/queue.php. Tanpa persiapan, Budi menerima email pengingat dua kali.

Obatnya: job yang idempoten, yaitu dijalankan dua kali hasilnya sama dengan sekali. Cek dulu apakah pekerjaannya sudah dilakukan:

public function handle(): void
{
    if ($this->loan->reminded_at !== null) {
        return; // sudah pernah dikirim, jangan kirim lagi
    }

    Mail::to($this->loan->user)->send(new DueReminder($this->loan));
    $this->loan->update(['reminded_at' => now()]);
}

Untuk yang lebih mahal dari email, misalnya pembayaran, pakai kunci unik per operasi. Polanya ada di lesson Idempotency & Retry Safety (track System Design).

⚠️ Jebakan yang sering ketemu

TL;DR: Queue menunda pekerjaan lambat ke proses lain. Buat job dengan php artisan make:job SendDueReminder, tandai implements ShouldQueue dan use Queueable, taruh pekerjaannya di handle(), lalu masukkan ke antrean dengan SendDueReminder::dispatch($loan). QUEUE_CONNECTION=database menyimpannya di tabel jobs, dan php artisan queue:work menjalankannya satu per satu (RUNNING, lalu DONE atau FAIL). $tries membatasi percobaan, $backoff memberi jeda, dan job yang tetap gagal masuk failed_jobs lalu failed() dipanggil. Job bisa jalan dua kali, jadi buat dia idempoten: cek dulu apakah pekerjaannya sudah dilakukan.

Yang akan kamu pelajari