Dalam sistem email transaksional, tidak semua email punya urgensi yang sama. Email OTP harus sampai dalam hitungan detik. Newsletter bisa delay 15 menit tanpa masalah. Kalau semua email diobati sama, email kritis tertunda karena antri di belakang email bernilai rendah. Priority queue memastikan email berprioritas tinggi diproses duluan. Throttling queue memastikan sistem tidak melebihi limit SMTP provider. Artikel ini membahas implementasi kedua pola tersebut di Laravel dan Node.js.
Daftar Isi
Kenapa Email Transaksional Butuh Priority dan Throttling Queue?
Di environment development, mengirim email terlihat mudah. Anda panggil fungsi kirim, email sampai. Tapi di production dengan volume tinggi, muncul masalah yang tidak terlihat di localhost.
Bayangkan skenario ini: aplikasi Anda mengirim 1.000 email per menit. Sebagian besar adalah newsletter dan notifikasi promote. Tapi di tengah antrian panjang tersebut, ada email OTP yang harus sampai dalam 10 detik. Kalau semua email diproses FIFO (first in, first out), email OTP harus menunggu di belakang 999 email lain yang tidak urgent.
Masalah lain yang sama seriusnya: limit SMTP provider. Gmail membatasi sekitar 100-200 email per detik per koneksi. Jika aplikasi Anda mengirim 500 email per detik tanpa throttling, 300 email akan gagal karena provider menolak koneksi baru.
Priority queue menyelesaikan masalah pertama. Throttling queue menyelesaikan masalah kedua. Keduanya saling melengkapi dalam arsitektur email yang production-ready.
Konsep Priority Queue untuk Email
Priority queue adalah pola antrian yang memproses item berdasarkan prioritas, bukan urutan masuk. Dalam konteks email, tiga level prioritas yang umum:
High priority (prioritas 1-10): OTP, password reset, 2FA code. Email ini harus diproses dalam hitungan detik. Keterlambatan 1 menit sudah terasa masalah.
Normal priority (prioritas 11-20): Invoice, konfirmasi order, notifikasi pembayaran. Email ini harus terkirim tapi delay 5-15 menit masih acceptable.
Low priority (prioritas 21-30): Newsletter, promo, notifikasi non-kritis. Delay 30 menit sampai beberapa jam tidak jadi masalah.
Implementasi paling sederhana adalah dengan membagi queue menjadi tiga separate queues. Laravel dan Bull (Node.js) keduanya support multiple queues secara native dengan built-in priority handling.
Implementasi Priority Queue di Laravel
Laravel punya queue system yang sudah solid dengan support untuk multiple queues dan priority weights. Struktur implementasi priority queue di Laravel dimulai dengan mendefinisikan queue untuk setiap priority level.
Pertama, konfigurasi queue di config/queue.php. Laravel mendukung driver Redis yang performanya paling tinggi untuk use case ini:
// config/queue.php
'connections' => [
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'retry_after' => 90,
'block_for' => 5,
],
],
Kedua, buat tiga queue configuration untuk tiga priority level. Buat file config/queues.php terpisah untuk clarity:
// config/queues.php
return [
'high' => [
'connection' => 'redis',
'queue' => 'email_high',
'max_retries' => 3,
'timeout' => 30,
],
'normal' => [
'connection' => 'redis',
'queue' => 'email_normal',
'max_retries' => 5,
'timeout' => 60,
],
'low' => [
'connection' => 'redis',
'queue' => 'email_low',
'max_retries' => 5,
'timeout' => 120,
],
];
Ketiga, buat job class untuk setiap tipe email dengan queue yang sesuai:
// app/Jobs/SendOtpEmail.php
class SendOtpEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
// Priority tinggi: diproses duluan
public $queue = 'email_high';
public $tries = 3;
public $backoff = [2, 5, 10]; // detik, retry cepat untuk OTP
// ... property dan handle method
}
// app/Jobs/SendInvoiceEmail.php
class SendInvoiceEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
// Priority normal
public $queue = 'email_normal';
public $tries = 5;
public $backoff = [30, 60, 120, 300, 600];
}
// app/Jobs/SendNewsletterEmail.php
class SendNewsletterEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
// Priority rendah
public $queue = 'email_low';
public $tries = 3;
public $backoff = [60, 300, 600];
}
Keempat, jalankan worker dengan konfigurasi yang memproses queue high lebih banyak concurrency dibanding queue low. Idealnya, Anda punya minimal 2 worker dedicated untuk queue high, 2 worker untuk normal, dan 1 worker untuk low:
# Worker untuk queue high priority (2 worker)
php artisan queue:work redis --queue=email_high --tries=3 --timeout=30 &
php artisan queue:work redis --queue=email_high --tries=3 --timeout=30 &
# Worker untuk queue normal (2 worker)
php artisan queue:work redis --queue=email_normal --tries=5 --timeout=60 &
# Worker untuk queue low (1 worker)
php artisan queue:work redis --queue=email_low --tries=3 --timeout=120
Dengan struktur ini, 2.000 email OTP akan diproses oleh 2 worker concurrently. Newsletter yang masuk bersamaan akan diproses oleh 1 worker dan tidak mengganggu ketersediaan worker untuk email kritis.
Implementasi Priority Queue di Node.js dengan Bull
Bull adalah queue library paling populer untuk Node.js. Bull menggunakan Redis sebagai backend dan punya fitur priority queue built-in. Priority di Bull range dari 1 sampai 100, dengan angka lebih rendah berarti prioritas lebih tinggi.
Pertama, setup Bull queue dengan tiga separate queues:
const Queue = require('bull');
const highPriorityQueue = new Queue('email-high', {
redis: { port: 6379, host: '127.0.0.1' },
defaultJobOptions: {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 },
removeOnComplete: true,
removeOnFail: false,
},
});
const normalPriorityQueue = new Queue('email-normal', {
redis: { port: 6379, host: '127.0.0.1' },
defaultJobOptions: {
attempts: 5,
backoff: { type: 'exponential', delay: 30000 },
removeOnComplete: true,
removeOnFail: false,
},
});
const lowPriorityQueue = new Queue('email-low', {
redis: { port: 6379, host: '127.0.0.1' },
defaultJobOptions: {
attempts: 3,
backoff: { type: 'exponential', delay: 60000 },
removeOnComplete: true,
removeOnFail: false,
},
});
Kedua, enqueue email ke queue yang sesuai berdasarkan priority:
async function sendOtpEmail(userEmail, otpCode) {
await highPriorityQueue.add(
{
type: 'otp',
to: userEmail,
subject: 'Kode OTP Anda',
body: `Kode OTP Anda adalah ${otpCode}. Berlaku selama 5 menit.`,
},
{ priority: 1 } // Prioritas tertinggi
);
}
async function sendInvoiceEmail(userEmail, invoiceData) {
await normalPriorityQueue.add(
{
type: 'invoice',
to: userEmail,
subject: `Invoice #${invoiceData.id}`,
body: invoiceData.htmlContent,
},
{ priority: 10 } // Prioritas medium
);
}
async function sendNewsletterEmail(subscriberEmail, campaignData) {
await lowPriorityQueue.add(
{
type: 'newsletter',
to: subscriberEmail,
subject: campaignData.subject,
body: campaignData.htmlContent,
},
{ priority: 50 } // Prioritas rendah
);
}
Ketiga, setup processor untuk setiap queue dengan concurrency yang berbeda:
// Processor untuk high priority: concurrency tinggi
highPriorityQueue.process(async (job) => {
console.log(`[HIGH] Processing OTP email untuk ${job.data.to}`);
await sendEmailViaSMTP(job.data);
return Promise.resolve();
});
// Processor untuk normal priority: concurrency medium
normalPriorityQueue.process(5, async (job) => {
console.log(`[NORMAL] Processing invoice untuk ${job.data.to}`);
await sendEmailViaSMTP(job.data);
return Promise.resolve();
});
// Processor untuk low priority: concurrency rendah
lowPriorityQueue.process(2, async (job) => {
console.log(`[LOW] Processing newsletter untuk ${job.data.to}`);
await sendEmailViaSMTP(job.data);
return Promise.resolve();
});
Dengan Bull, queue dengan priority lebih rendah tidak akan diproses jika ada job pending di queue dengan prioritas lebih tinggi, meskipun worker low priority sedang idle. Ini penting untuk memastikan email kritis tidak pernah bersaing dengan email non-kritis.
Throttling Queue: Menjaga Beban di Bawah Limit SMTP
Priority queue memastikan email kritis diproses duluan. Throttling queue memastikan sistem tidak membanjiri SMTP provider dengan request yang melebihi kapasitasnya. Throttling berbeda dari retry: retry menangani kegagalan temporary, throttling mencegah kegagalan karena overload.
Implementasi throttling queue yang paling efektif menggunakan token bucket algorithm. Konsepnya sederhana: ada bucket yang berisi token. Setiap email yang dikirim consumes satu token. Token replenished dengan rate terbatas. Jika bucket kosong, email harus wait sampai ada token baru.
Implementasi token bucket di Node.js:
class ThrottleQueue {
constructor(options = {}) {
this.capacity = options.capacity || 10; // max token dalam bucket
this.refillRate = options.refillRate || 10; // token per detik
this.tokens = this.capacity;
this.lastRefill = Date.now();
this.queue = [];
this.processing = false;
}
async addEmail(emailJob) {
return new Promise((resolve, reject) => {
this.queue.push({ emailJob, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
while (this.queue.length > 0) {
await this.refill();
if (this.tokens < 1) {
// Bucket kosong, wait sebentar sebelum cek lagi
await this.sleep(100);
continue;
}
const item = this.queue.shift();
this.tokens -= 1;
// Process email
this.processEmail(item).then(item.resolve).catch(item.reject);
}
this.processing = false;
}
async refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000; // detik
const tokensToAdd = elapsed * this.refillRate;
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefill = now;
}
async processEmail(item) {
const { emailJob } = item;
try {
await sendEmailViaSMTP(emailJob);
} catch (error) {
// Handle SMTP error
if (isThrottleError(error)) {
// Throttle error: requeue dengan delay
await this.sleep(5000);
this.queue.unshift(item);
} else {
throw error;
}
}
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
Untuk penggunaan, wrap setiap SMTP send operation dengan throttle queue:
const throttleQueue = new ThrottleQueue({
capacity: 10, // Maks 10 email concurent
refillRate: 10, // 10 email per detik
});
async function sendEmailThrottled(emailJob) {
return throttleQueue.addEmail(emailJob);
}
Pengaturan refill rate harus disesuaikan dengan limit SMTP provider. Gmail dan Microsoft 365 biasanya mengijinkan 100-200 email per detik per koneksi. KIRIM.EMAIL menyediakan dashboard untuk melihat limit dan usage aktual agar Anda bisa tuning throttle rate dengan tepat.
Kapan Split ke Priority Queue, Kapan Cukup Retry Saja?
Tidak semua sistem butuh priority queue. Split queue menambah kompleksitas operasional. Ada criteria jelas kapan split queue worth it.
Split queue necessary jika: First, ada email yang punya SLA ketat. OTP harus sampai dalam 10 detik. Kalau tidak, user experience langsung terganggu dan Anda dapat support ticket. Kedua, volume email non-kritis jauh lebih besar dari email kritis. Jika 80% email Anda adalah newsletter dan 20% adalah OTP, newsletter bisa delay tapi tidak boleh mengganggu OTP.
Cukup retry mechanism saja jika: semua email punya urgency yang sama, tidak ada SLA ketat, dan volume total rendah (di bawah 100 email per menit). Dalam kasus ini, retry exponential backoff sudah cukup untuk handle temporary failure tanpa perlu complexity tambahan.
Kesalahan yang sering dilakukan adalah over-engineering: split queue untuk sistem yang tidak butuh. Anda tidak perlu 3 level queue kalau semua email sama urgent. Mulai dari satu queue dengan retry mechanism yang solid, baru tambah complexity jika ada masalah yang measured.
Kalau masalahnya adalah SMTP provider yang sering throttle padahal queue sudah dioptimize, pertimbangkan untuk pakai SMTP relay dengan managed IP yang jauh lebih baik. Relay memberikan kontrol penuh atas reputation dan limit tanpa harus manage queue infrastructure yang kompleks.
Kalau masalahnya lebih ke setup queue yang kompleks untukhandle priority dan throttling sekaligus, KIRIM.EMAIL Dev menyediakan queue management bawaan dengan per-priority processing dan automatic throttling berdasarkan limit akun Anda.
FAQ
Apa bedanya priority queue dan regular queue untuk email?
Regular queue (FIFO) memproses email berdasarkan urutan masuk. Semua email punya kesempatan yang sama untuk diproses. Priority queue memproses email berdasarkan urgensi. Email dengan priority tinggi diproses duluan meskipun masuk depois email priority rendah. Dalam praktiknya, sistem email transaksional butuh keduanya: priority queue untuk memastikan email kritis diproses duluan, dan throttle queue untuk memastikan tidak melebihi limit SMTP.
Bagaimana cara implement throttle queue agar tidak melebihi SMTP limit?
Throttle queue menggunakan token bucket algorithm: ada bucket yang berisi token, setiap email yang dikirim consumes satu token, dan token replenished dengan rate terbatas. Implementasi basic bisa pakai library seperti Bottleneck di Node.js atau RateLimiter di Laravel. Kunci penggunaannya adalah set refill rate sesuai dengan limit SMTP provider, tidak lebih.
Kapan harus split email jadi priority queue dan regular queue?
Split ketika ada email dengan SLA berbeda. Email OTP, password reset, dan 2FA code harus sampai dalam hitungan detik. Email newsletter dan promo bisa delay 30 menit tanpa masalah. Jika semua email punya urgency yang sama, split queue tidak diperlukan.
Bagaimana implementasi priority queue di Laravel?
Gunakan multiple queues dengan nama berbeda: email_high, email_normal, email_low. Set property $queue di job class untuk assign queue. Jalankan worker terpisah untuk setiap queue dengan concurrency berbeda. Worker untuk high priority queue biasanya perlu lebih banyak concurrent worker dibanding low priority queue.
Apakah Redis sorted set bisa dipakai untuk priority queue?
Ya. Gunakan score = (priority * 1000000) + timestamp untuk sorting. Angka score lebih rendah = prioritas lebih tinggi. Contoh: score untuk OTP = (1 * 1000000) + 1700000000 = 1700000001, score untuk newsletter = (20 * 1000000) + 1700000000 = 1700000020. Redis akan sort berdasarkan score dan mengambil email OTP duluan.
Bagaimana cara monitoring queue depth untuk setiap priority level?
Gunakan metric berikut: queue depth untuk setiap level, processing time per job, failure rate per queue, dan retry distribution. Di Laravel, Anda bisa pakai package seperti Laravel Horizon untuk visualisasi queue secara real-time. Di Node.js, Bull menyediakan API untuk get queue stats.
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.
- Cara Implementasi Email Priority Queue dan Throttling Queue untuk Sistem Transaksional High-Volume - April 21, 2026
- Cara Kirim Email dengan Attachment di Aplikasi Web: Size Limit, MIME Type, dan Security Best Practice - April 20, 2026
- Cara Konfigurasi Mail Driver di Laravel untuk Email Transaksional Production - April 19, 2026
