Mail::send() dan Mail::queue() bukan dua cara menulis hal yang sama dengan syntax berbeda. Keduanya punya model failure yang fundamentally berbeda. Mail::send() throw exception saat SMTP timeout atau credential salah. Mail::queue() dispatch job ke queue, retry maksimal 3 kali dengan backoff random 0-180 detik, lalu diam-diam hapus job kalau gagal semua tanpa memberi tahu siapa pun. Anda perlu memahami perbedaan ini sebelum production incident terjadi.
Daftar Isi
Apa Perbedaan Dasar Mail::send() dan Mail::queue()?
Mail::send() adalah synchronous call. Ketika Anda memanggil Mail::send(), kode Anda berhenti di baris itu sampai email benar-benar terkirim atau gagal. Kalau SMTP server timeout, credential salah, atau koneksi gagal, Laravel melempar exception dan Anda bisa menangkapnya dengan try-catch. Ini berbeda dengan menggunakan email transaksional via queue yang berjalan secara asynchronous.
Mail::queue() tidak mengirim email secara langsung. Yang terjadi adalah Laravel membuat job, menyimpan job itu ke queue, dan langsung mengembalikan eksekusi ke kode Anda. Email dikirim kemudian oleh queue worker yang berjalan terpisah. Kalau queue worker belum jalan, email tidak akan pernah terkirim dan tidak ada error di mana pun. Ini berbeda dari pendekatan role-based email di mana setiap tipe email punya handling yang berbeda.
Perbedaan fundamental ini menentukan bagaimana failure handling seharusnya bekerja di masing-masing pendekatan.
Kenapa Mail::queue() Tidak Throw Exception Saat Gagal?
Mail::queue() tidak throw exception karena memang bukan tempat email dikirim. Yang dilempar ke queue adalah instruksi untuk mengirim email, bukan email itu sendiri. Exception handling dari Mail::queue() terjadi di level job worker, bukan di level kode utama Anda.
Ketika queue worker memproses job email dan SMTP gagal, worker akan catch exception tersebut, bukan lempar ke kode yang dispatch. Worker akan retry job itu maksimal 3 kali dengan jeda backoff random antara 0 sampai 180 detik. Setelah 3 kali retry tetap gagal, job akan dihapus dari queue tanpa notification.
GitHub Issue #3734 di laravel/framework mendokumentasikan kasus di mana Mail::queue() berperilaku berbeda dari Mail::send() dalam hal exception handling. Perbedaan ini bukan bug, melainkan desain: queue mengasumsikan failure sementara dan expecting retry.
Bagaimana Retry Mechanism Bekerja di Mail::queue()?
Setiap job yang di-dispatch via Mail::queue() memiliki attribute tries yang default-nya adalah 3. Ketika job gagal, Laravel menghitung kembali tries yang tersisa. Jika masih ada kesempatan retry, job dikembalikan ke queue dengan jeda backoff.
Backoff default di Laravel menggunakan formula acak: antara 0 dan 180 detik. Artinya setiap retry bisa terjadi 30 detik kemudian, atau 2 menit kemudian, atau interval acak lainnya dalam rentang itu. Ini bukan bug tapi desain untuk mencegah thundering herd problem di mana ribuan job semuanya retry bersamaan ketika satu SMTP server kembali online.
Timeout default untuk setiap queue job adalah 60 detik. Kalau job tidak selesai dalam 60 detik, worker menganggap job gagal dan melakukan retry. Kalau job masih gagal setelah semua tries habis, job masuk ke failed_jobs table jika tabel tersebut sudah dimigrate.
Kenapa Email Tidak Masuk tapi queue:failed Kosong?
Ini adalah skenario tersering di production. Anda tahu email tidak sampai ke pengguna. Anda jalankan php artisan queue:failed dan hasilnya kosong. Lalu apa yang terjadi?
Kemungkinan pertama: queue worker tidak pernah dijalankan. Mail::queue() berhasil dispatch job ke database, tapi tidak ada proses yang memproses queue tersebut. Job duduk di tabel jobs sampai dijadwalkan dihapus oleh queue:prune.
Kemungkinan kedua: failed_jobs table tidak dimigrate. Ketika job gagal dan tidak ada tries tersisa, Laravel berusaha menyimpan job ke failed_jobs table. Kalau tabel tersebut tidak ada, Laravel tidak bisa menyimpan record dan job hilang tanpa jejak.
Kemungkinan ketiga: job berhasil dikirim tapi SMTP server tujuan menolaknya. Queue worker menganggap email berhasil terkirim ke SMTP server. Yang SMTP server tujuan tolak email setelah menerima, itu tidak terlihat di sisi Laravel. Email bisa masuk spam atau ditolak sebelum mencapai inbox.
Kapan Harus Pakai Mail::queue() dan Kapan Pakai Mail::send()?
Gunakan Mail::send() ketika email harus terkirim sebelum user melihat response. Contohnya email OTP atau password reset yang langsung dibutuhkan user. Kalau email ini gagal, user harus tahu saat itu juga.
Gunakan Mail::queue() ketika email adalah consequences dari aksi yang sudah selesai. Contohnya email notifikasi order, welcome email, atau laporan harian. User tidak membutuhkan email ini secara realtime. Kalau email ini gagal dan di-retry beberapa kali kemudian berhasil, itu masih acceptable.
Mail::queue() juga tepat ketika volume email tinggi. Mengirim 1.000 email secara synchronous dengan Mail::send() akan memblokir request selama mungkin 1 jam. Dengan Mail::queue(), request kembali ke user dalam milidetik dan email diproses di background.
Bagaimana Setup Retry dengan Delay untuk Mail::queue()?
Untuk mailables yang implements ShouldQueue, Anda bisa mengatur tries dan backoff langsung di class:
class OrderShippedMail extends Mailable implements ShouldQueue
{
use Queueable;
public int $tries = 5;
public array $backoff = [30, 60, 120, 240, 480];
}
Dengan konfigurasi ini, job akan retry 5 kali dengan backoff exponential: 30 detik, 1 menit, 2 menit, 4 menit, dan 8 menit. Ini memberi SMTP server waktu untuk recover dari outage tanpa membanjiri queue dengan retry yang bersamaan.
Untuk mailables yang tidak implements ShouldQueue, Anda bisa pakai Mail::queue() dengan delay:
Mail::to($user)->queue(new OrderShippedMail($order))->delay(now()->addMinutes(5));
Delay ini menambah jeda sebelum job pertama kali diproses, tapi tidak mengubah retry backoff mechanism.
Kenapa Memahami Behavior Difference Ini Penting untuk Production?
Production incident yang melibatkan email biasanya memakan waktu lebih lama untuk didiagnosis dibanding masalah lain. Email yang tidak sampai tidak langsung terlihat di dashboard aplikasi. User harus komplain dulu sebelum Anda tahu ada masalah.
Kalau Anda pakai Mail::send() dan email tidak sampai, exception akan terlihat di log dan Anda bisa langsung tahu ada masalah. Tapi kalau Anda pakai Mail::queue() dan email tidak sampai, job bisa gagal secara silent setelah 3 kali retry. Tidak ada error di log aplikasi karena failure terjadi di worker process, bukan di request utama.
Contoh konkret: SMTP server KIRIM.EMAIL yang Anda gunakan tiba-tiba unreachable selama 30 detik. Kalau pakai Mail::send(), request akan timeout dan Anda bisa menangkap exception dengan retry logic di level aplikasi. Kalau pakai Mail::queue() dengan default 3 tries dan backoff 0-180 detik, job akan retry secara random dalam rentang waktu itu dan mungkin berhasil ketika koneksi pulih. Tapi kalau SMTP server down selama 10 menit, job akan gagal 3 kali dan dihapus tanpa notification.
Kalau Anda butuh SMTP provider yang monitoring queue-nya lebih transparan, coba KIRIM.EMAIL Dev.
FAQ
Apa bedanya Mail::send() dan Mail::queue()?
Mail::send() mengirim email secara synchronous, blocking kode sampai email terkirim atau gagal dengan exception. Mail::queue() dispatch job ke queue dan langsung mengembalikan eksekusi ke kode, email diproses kemudian oleh queue worker.
Kenapa Mail::queue() tidak throw exception saat gagal?
Mail::queue() bukan mengirim email, hanya mendispatch instruksi kirim email ke queue. Exception handling terjadi di queue worker process, bukan di kode yang memanggil Mail::queue(). Worker akan retry job sesuai tries yang tersisa, bukan melempar exception ke kode utama.
Bagaimana cara handle failure dari Mail::queue()?
Pastikan failed_jobs table sudah dimigrate dengan php artisan queue:failed-table && migrate. Job yang gagal akan tercatat di tabel tersebut. Anda juga bisa listen ke event JobFailed di ServiceProvider untuk mengirim alert atau logging secara custom.
Kapan harus pakai queue untuk email?
Pakai queue untuk email yang tidak dibutuhkan secara realtime oleh user, seperti welcome email, notification, atau laporan. Queue juga tepat untuk volume tinggi di mana mengirim banyak email secara synchronous akan memblokir request terlalu lama.
Bagaimana setup retry dengan delay?
Untuk mailable yang implements ShouldQueue, atur property tries dan backoff array di class. Untuk mailables biasa, gunakan method delay() saat memanggil queue(). Retry backoff default di Laravel adalah random 0-180 detik per percobaan.
Hasbi Putra adalah Head of Marketing di KIRIM.EMAIL, email delivery infrastructure untuk developer dan tim IT di Indonesia. KIRIM.EMAIL mengirim lebih dari 11 juta email per hari dengan server yang sepenuhnya berlokasi di Indonesia.
