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 = 3dan$backoff = [10, 60]. Worker mencobanya pertama kali jam 09:00:00. Kapan job itu menyerah dan pindah ke tabelfailed_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));
make:jobmembuatapp/Jobs/SendDueReminder.php.implements ShouldQueue: job ini masuk antrean, bukan dijalankan saat itu juga.use Queueablememberidispatch(),delay(), danonQueue(). Model di constructor disimpan sebagai id-nya saja, lalu diambil ulang dari database saat job jalan.handle()berisi pekerjaannya. Yang memanggilnya worker, bukan kamu.SendDueReminder::dispatch($loan)menulis satu baris ke antrean. Antreannya ditentukanQUEUE_CONNECTIONdi.env: project baru memakaidatabase(tabeljobs, migrasinya sudah ada). Nilaisyncmenjalankan job langsung, tanpa antrean.
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
- Tiap job menulis dua baris:
RUNNINGsaat diambil, lalu lama kerjanya danDONE(berhasil) atauFAIL(gagal). - Job diambil urut, yang paling lama menunggu dulu. Yang
DONEdihapus dari tabeljobs. - Worker terus menyala dan menunggu job baru sampai kamu menekan
Ctrl+C. Ada jugaphp artisan queue:listen, yang membaca ulang kodemu tiap job: lebih lambat, enak untuk development. - Di server, worker dijaga oleh process manager supaya hidup lagi kalau mati. Caranya di lesson Systemd & Service Management (track DevOps).
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.
- Tanpa
$tries, job cuma dicoba sekali (kecuali worker dijalankan dengan--tries=3). Tanpa backoff, percobaan berikutnya langsung. - Laravel 13 juga menerima atribut
#[Tries(3)]dan#[Backoff([10, 60])]di atas class. Artinya sama. php artisan queue:failedmenampilkan isifailed_jobs. Setelah server email hidup lagi,php artisan queue:retry allmemasukkan semuanya kembali ke antrean.
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
- Worker gak jalan.
dispatch()berhasil, tapi job cuma menumpuk di tabeljobsdan email gak pernah terkirim. - Kode baru, worker lama. Worker menyimpan kodemu di memori sejak dia menyala. Setelah deploy, jalankan
php artisan queue:restart. - Lupa
implements ShouldQueue, atauQUEUE_CONNECTION=sync. Job jalan saat itu juga, dan user tetap menunggu. - Di latihan, gak ada yang dicetak.
handle()di latihan mengembalikan teks, jadi cetak hasilnya:echo $job->handle();. Methodfailed()juga harus ada.
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.