Pendahuluan
Bayangkan kamu membangun fitur “daftar akun” di aplikasi Laravel. Setelah user submit form, sistem harus mengirim email selamat datang. Hasilnya? User harus menunggu 3–5 detik sampai server selesai mengirim email, baru halaman berhasil muncul.
Pengalaman buruk ini terjadi karena PHP mengeksekusi semuanya secara synchronous — satu per satu, berurutan. Kirim email selesai, baru response dikembalikan ke browser.
Solusinya adalah Queue dan Jobs:
Tanpa Queue:
User → Request → Kirim Email (3 detik) → Response ← User menunggu lama!
Dengan Queue:
User → Request → Simpan Job ke Queue → Response (instan!)
│
Worker (background) → Kirim Email
Di artikel ini, kita akan belajar cara kerja Queue di Laravel dari nol sampai production-ready dengan Supervisor.
Konsep: Synchronous vs Asynchronous
Synchronous (default PHP): Setiap baris kode dieksekusi berurutan. Jika satu langkah lambat, semua ikut menunggu.
Asynchronous dengan Queue: Tugas berat “didorong” ke antrian, worker memproses di background. Request selesai instan, user tidak perlu menunggu.
Analogi: Bayangkan kamu di restoran cepat saji. Kasir mencatat pesananmu, kamu langsung bayar dan dapat nomor antrean. Kamu tidak berdiri di depan kasir menunggu burger dimasak — itu terjadi di dapur (background). Saat siap, namamu dipanggil.
Kapan butuh Queue?
- Kirim email atau SMS
- Generate PDF/laporan
- Proses upload dan resize gambar
- Panggil API eksternal yang lambat
- Sinkronisasi data ke layanan ketiga
Queue Driver yang Tersedia
| Driver | Kegunaan | Kecepatan | Cocok untuk |
|---|---|---|---|
sync | Eksekusi langsung (tidak async) | N/A | Development/testing |
database | Simpan job di tabel MySQL | Sedang | Proyek kecil-menengah |
redis | In-memory queue | Cepat | Production, traffic tinggi |
sqs | Amazon SQS | Sangat cepat | AWS infrastructure |
beanstalkd | Dedicated queue server | Cepat | Server tertentu |
Untuk memulai, gunakan database. Setelah traffic meningkat, migrasi ke redis tanpa ubah kode apapun.
Langkah 1: Konfigurasi Queue Driver
Gunakan Database Driver (Permulaan)
# Install tabel queue di database
php artisan queue:table
php artisan migrate
# .env
QUEUE_CONNECTION=database
Upgrade ke Redis (Production)
# Install Redis di Ubuntu
sudo apt install redis-server
# Install package PHP Redis
composer require predis/predis
# .env
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
Langkah 2: Membuat Job Pertama
php artisan make:job SendWelcomeEmail
File app/Jobs/SendWelcomeEmail.php otomatis dibuat:
<?php
// app/Jobs/SendWelcomeEmail.php
namespace App\Jobs;
use App\Mail\WelcomeMail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
// Berapa kali coba jika gagal
public int $tries = 3;
// Timeout dalam detik
public int $timeout = 60;
public function __construct(
private User $user // Model dikirim melalui constructor
) {}
public function handle(): void
{
// Kode ini dieksekusi oleh worker di background
Mail::to($this->user->email)
->send(new WelcomeMail($this->user));
}
// Dipanggil setelah semua percobaan gagal
public function failed(\Throwable $exception): void
{
// Kirim notifikasi ke developer atau catat ke log
\Log::error("SendWelcomeEmail failed for user {$this->user->id}: {$exception->getMessage()}");
}
}
Penjelasan implements ShouldQueue: Interface ini memberitahu Laravel bahwa class ini harus dimasukkan ke antrian, bukan dieksekusi langsung. Tanpa interface ini, job langsung dieksekusi synchronous.
Langkah 3: Dispatch Job dari Controller
<?php
// app/Http/Controllers/Auth/RegisterController.php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Jobs\SendWelcomeEmail;
use App\Models\User;
use Illuminate\Http\Request;
class RegisterController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => bcrypt($validated['password']),
]);
// Dispatch job — masuk ke antrian, tidak blocking
SendWelcomeEmail::dispatch($user);
// Response langsung dikembalikan tanpa menunggu email terkirim
return redirect('/dashboard')->with('success', 'Akun berhasil dibuat!');
}
}
Variasi Dispatch
// Tunda eksekusi 5 menit dari sekarang
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));
// Kirim ke queue tertentu (misal: high-priority)
SendWelcomeEmail::dispatch($user)->onQueue('high');
// Eksekusi setelah response dikirim ke browser
SendWelcomeEmail::dispatchAfterResponse($user);
Langkah 4: Menjalankan Worker
# Mode development — otomatis reload setiap ada perubahan kode
php artisan queue:listen
# Mode production — lebih cepat, tapi perlu restart manual setelah deploy
php artisan queue:work
# Opsi penting
php artisan queue:work \
--tries=3 \ # Maksimal 3 percobaan per job
--timeout=60 \ # Timeout 60 detik per job
--sleep=3 \ # Tunggu 3 detik jika queue kosong
--queue=high,default # Proses queue 'high' dulu, lalu 'default'
queue:work vs queue:listen:
queue:work: Lebih efisien, load kode sekali. Wajib restart setelah deploy.queue:listen: Auto-reload kode. Bagus untuk development, terlalu boros untuk production.
Langkah 5: Menangani Job yang Gagal
Buat tabel failed jobs:
php artisan queue:failed-table
php artisan migrate
# Lihat semua job yang gagal
php artisan queue:failed
# Retry semua job yang gagal
php artisan queue:retry all
# Retry job tertentu (berdasarkan ID)
php artisan queue:retry 5
# Hapus job yang gagal
php artisan queue:flush
Tambahkan di AppServiceProvider agar dapat notifikasi saat job gagal:
<?php
// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Queue;
use Illuminate\Queue\Events\JobFailed;
public function boot(): void
{
Queue::failing(function (JobFailed $event) {
// Kirim alert ke Slack atau email developer
\Log::critical('Queue job failed', [
'job' => $event->job->getName(),
'exception' => $event->exception->getMessage(),
]);
});
}
Langkah 6: Job Chaining — Jalankan Job Berurutan
Gunakan Job Chain jika satu job harus selesai sebelum job berikutnya dimulai:
use Illuminate\Support\Facades\Bus;
use App\Jobs\GenerateInvoicePdf;
use App\Jobs\SendInvoiceEmail;
use App\Jobs\UpdateOrderStatus;
// PDF dibuat dulu, lalu email dikirim, lalu status di-update
Bus::chain([
new GenerateInvoicePdf($order),
new SendInvoiceEmail($order),
new UpdateOrderStatus($order, 'completed'),
])->dispatch();
Jika satu job dalam chain gagal, job berikutnya tidak akan dijalankan.
Langkah 7: Supervisor untuk Production
Supervisor memastikan queue worker selalu berjalan — jika worker crash, supervisor otomatis restart.
sudo apt install supervisor
sudo nano /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/task-manager/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=deploy
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/task-manager/storage/logs/worker.log
stopwaitsecs=3600
# Aktifkan konfigurasi
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
# Cek status
sudo supervisorctl status
numprocs=2 artinya 2 worker berjalan paralel — cocok untuk server 1–2 vCPU. Server lebih kuat? Naikan ke 4–8.
Setelah deploy (jalankan setelah php artisan optimize):
# Beritahu worker untuk restart setelah proses job saat ini selesai
php artisan queue:restart
Contoh Lengkap: Sistem Email Selamat Datang
Buat Mailable
php artisan make:mail WelcomeMail --markdown=emails.welcome
<?php
// app/Mail/WelcomeMail.php
namespace App\Mail;
use App\Models\User;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class WelcomeMail extends Mailable
{
use SerializesModels;
public function __construct(public User $user) {}
public function envelope(): Envelope
{
return new Envelope(subject: 'Selamat Datang di KamusNgoding!');
}
public function content(): Content
{
return new Content(markdown: 'emails.welcome');
}
}
{{-- resources/views/emails/welcome.blade.php --}}
@component('mail::message')
# Halo, {{ $user->name }}!
Selamat datang di **KamusNgoding**! 🎉
Akun kamu sudah aktif dan siap digunakan.
@component('mail::button', ['url' => url('/dashboard')])
Mulai Belajar
@endcomponent
Salam,<br>
Tim KamusNgoding
@endcomponent
Konfigurasi Mail (Mailtrap untuk testing)
# .env
MAIL_MAILER=smtp
MAIL_HOST=sandbox.smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=your_mailtrap_username
MAIL_PASSWORD=your_mailtrap_password
MAIL_FROM_ADDRESS[email protected]
MAIL_FROM_NAME="${APP_NAME}"
Troubleshooting: Error yang Sering Muncul
Job sudah di-dispatch tapi tidak dieksekusi
Penyebab: Worker tidak berjalan, atau QUEUE_CONNECTION=sync yang langsung mengeksekusi.
Solusi:
# Cek apakah worker berjalan
php artisan queue:work
# Pastikan driver bukan sync
grep QUEUE_CONNECTION .env
# Harus: QUEUE_CONNECTION=database atau redis
# Lihat isi queue saat ini
php artisan queue:monitor
Illuminate\Queue\MaxAttemptsExceededException
Penyebab: Job gagal lebih dari jumlah tries yang ditentukan.
Solusi:
// Di Job class, naikan tries atau tambahkan backoff
public int $tries = 5;
// Jeda antar percobaan (detik)
public array $backoff = [10, 30, 60];
Redis connection refused
Penyebab: Redis belum diinstall atau service tidak berjalan.
Solusi:
# Cek status Redis
sudo systemctl status redis
# Start Redis
sudo systemctl start redis
sudo systemctl enable redis
# Test koneksi
redis-cli ping
# Harus output: PONG
Job class not found setelah deployment
Penyebab: Worker masih load kode lama sebelum deploy.
Solusi:
# Setelah deploy, restart worker
composer dump-autoload
php artisan queue:restart
Pertanyaan yang Sering Diajukan
Kapan harus menggunakan Queue, kapan cukup synchronous?
Gunakan Queue jika operasi membutuhkan lebih dari 500ms atau bisa gagal karena faktor eksternal (jaringan, API pihak ketiga). Kirim email, proses gambar, panggil API — semua cocok untuk Queue. Operasi sederhana seperti simpan data ke database tidak perlu Queue.
Redis vs Database driver, mana yang lebih baik untuk production?
Redis jauh lebih cepat karena beroperasi di memory (RAM). Untuk production dengan lebih dari 50 job per menit, Redis adalah pilihan tepat. Database driver lebih mudah di-setup dan cocok untuk aplikasi dengan traffic rendah karena tidak butuh Redis terpisah.
Bagaimana cara monitoring Queue di production?
Gunakan Laravel Horizon (khusus Redis) untuk dashboard monitoring yang indah, atau Laravel Telescope untuk monitoring umum. Untuk yang sederhana, pantau dengan php artisan queue:monitor dan php artisan queue:failed.
Apakah Queue bisa digunakan tanpa Redis?
Ya! database driver adalah alternatif yang solid — job disimpan di tabel jobs di MySQL/PostgreSQL. Tidak perlu install Redis. Untuk produksi kecil-menengah, database driver sudah lebih dari cukup.
Kesimpulan
Queue dan Jobs adalah salah satu fitur paling impactful di Laravel untuk pengalaman pengguna. Dengan menggeser operasi berat ke background:
- Response instan — user tidak lagi menunggu email terkirim
- Resilient — gagal sekali? Otomatis retry
- Scalable — tambah worker saat traffic meningkat, tanpa ubah kode
Kamu sudah belajar cara kerja Queue dari driver, membuat Job, dispatch, menangani kegagalan, hingga setup Supervisor di production. Selanjutnya, di artikel Midtrans, kita akan menggunakan Queue untuk memproses webhook pembayaran — kombinasi yang sangat powerful untuk e-commerce!
Selamat — kamu sudah naik satu level sebagai Laravel developer! 🚀