Email queue dengan background worker adalah pola arsitektur yang memisahkan proses pengiriman email dari request utama aplikasi. Dengan BullMQ dan Redis, Anda bisa menangani ribuan email per menit tanpa membuat server API lambat. Artikel ini membahas setup production-ready: retry dengan exponential backoff, priority queue, dead letter handling, dan monitoring real-time.
Daftar Isi
Kenapa Email Queue Itu Penting untuk Aplikasi High-Volume
Email transaksional di aplikasi modern bukan sekadar nodemailer.send() di dalam request handler. Kalau ada 10.000 user yang checkout sekaligus di saat flash sale, server API akan langsung runtuh jika semua email dikirim secara synchronous.
Email queue memindahkan proses pengiriman ke background worker. Request API hanya perlu push job ke queue, worker mengambil job sesuai kapasitas, dan email terkirim satu per satu tanpa membebani sistem utama.
Beberapa keuntungan nyata:
- Throughput stabil. Server API tidak pernah blocked oleh operasi SMTP yang lambat.
- Retry otomatis. Email yang gagal karena masalah network sementara bisa di-retries secara otomatis.
- Monitoring yang jelas. Anda bisa lihat berapa email pending, berapa berhasil, berapa gagal.
- Priority handling. Email OTP bisa diproses lebih dulu daripada email newsletter.
Setup Project dengan BullMQ dan Redis
Pertama, install dependencies yang diperlukan:
npm install bullmq ioredis nodemailer
Redis adalah message broker untuk BullMQ. Anda bisa menggunakan Redis dari layanan cloud seperti Upstash, Railway, atau Redis Labs. Untuk development lokal, cukup jalankan Redis via Docker:
docker run -d -p 6379:6379 redis/redis-stack:latest
Berikut adalah konfigurasi koneksi Redis di project Node.js:
import { Queue, Worker } from 'bullmq';
import Redis from 'ioredis';
const connection = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: 6379,
maxRetriesPerRequest: null,
});
export const emailQueue = new Queue('email-sending', { connection });
maxRetriesPerRequest: null diperlukan karena BullMQ menggunakan blocking command Redis yang tidak support timeout.
Membuat Fungsi Pengirim Email dengan Nodemailer
Pisahkan logic pengiriman email menjadi fungsi terpisah yang bisa dipanggil oleh worker:
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || '587'),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
export async function sendEmailJob(job) {
const { to, subject, html, from } = job.data;
try {
await transporter.sendMail({
from: from || process.env.SMTP_FROM,
to,
subject,
html,
});
return { success: true, messageId: Date.now().toString() };
} catch (error) {
throw error;
}
}
Fungsi ini akan dipanggil oleh worker, bukan dipanggil langsung dari route handler.
Mendefinisikan Worker dengan Retry Strategy
Worker adalah proses yang mengambil job dari queue dan mengeksekusinya. Di sinilah retry strategy yang proper menjadi kritis:
export const emailWorker = new Worker(
'email-sending',
async (job) => {
console.log(`Processing job ${job.id}`);
return await sendEmailJob(job);
},
{
connection,
concurrency: 10,
limiter: {
max: 100,
duration: 1000,
},
retryStrategy: (tries) => {
if (tries > 5) {
return null;
}
return Math.min(tries * 2000, 30000);
},
}
);
emailWorker.on('completed', (job, result) => {
console.log(`Job ${job.id} completed:`, result);
});
emailWorker.on('failed', (job, err) => {
console.error(`Job ${job.id} failed:`, err.message);
});
Beberapa poin penting dari konfigurasi di atas:
concurrency: 10artinya worker memproses 10 email secara bersamaan. Sesuaikan dengan kapasitas server SMTP Anda.limiter: { max: 100, duration: 1000 }memastikan tidak lebih dari 100 email per detik. Ini mencegah Anda dari kena rate limit SMTP provider.retryStrategymenggunakan incremental delay: 2 detik, 4 detik, 6 detik, dst. Maksimum 30 detik antar retry. Setelah 5 kali gagal, job masuk ke failed state.
Priority Queue untuk Email Urgent
Tidak semua email punya urgensi yang sama. Email OTP harus sampai dalam hitungan detik, sedangkan email newsletter bisa menunggu. BullMQ mendukung priority queue dengan level 1-10:
import { Queue, Worker } from 'bullmq';
const emailQueue = new Queue('email-sending', {
connection,
defaultJobOptions: {
priority: 5,
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000,
},
},
});
export async function sendEmailWithPriority({ to, subject, html, priority = 5 }) {
await emailQueue.add(
'send-email',
{ to, subject, html },
{
priority,
jobId: `email-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
}
);
}
Untuk email OTP, gunakan priority 10. Untuk email notification biasa, priority 5. Untuk email newsletter, priority 1 atau 2.
Dead Letter Queue: Menangani Email yang Gagal Total
Ketika sebuah job gagal setelah maksimal retries, job tersebut masuk ke dead letter state. BullMQ menyediakan event untuk menangani ini:
emailWorker.on('failed', async (job, err) => {
if (job && job.attemptsMade >= (job.opts.retryStrategy ? 5 : 3)) {
console.log(`Job ${job.id} moved to dead letter after ${job.attemptsMade} attempts`);
const deadLetterQueue = new Queue('email-dead-letter', { connection });
await deadLetterQueue.add('failed-email', {
originalJob: job.data,
error: err.message,
attemptsMade: job.attemptsMade,
failedAt: new Date().toISOString(),
});
}
});
Dengan dead letter queue, Anda tidak pernah kehilangan email yang gagal. Anda bisa inspect kenapa email tersebut gagal dan manually requeue jika perlu.
Monitoring dengan BullMQ Dashboard
BullMQ menyediakan dashboard bawaan untuk monitoring queue. Install dengan:
npm install @bull-board/express4 bull-board
Setup dashboard:
import { createBullBoard } from '@bull-board/express';
import { BullMQAdapter } from '@bull-board/express/bullMQAdapter';
const { router, setQueues, addQueue } = createBullBoard([
new BullMQAdapter(emailQueue),
new BullMQAdapter(deadLetterQueue),
]);
app.use('/admin/queues', router);
Sekarang Anda bisa akses http://your-server:3000/admin/queues untuk melihat:
- Jumlah job pending, active, completed, failed
- Detail setiap job
- Retry atau delete job secara manual
- Lihat error message dari job yang gagal
Memicu Email Queue dari Route Handler
Di route handler, Anda tidak perlu tunggu email terkirim. Cukup push ke queue dan langsung return response ke client:
import express from 'express';
import { sendEmailWithPriority } from './email-queue.js';
const app = express();
app.post('/checkout', async (req, res) => {
const { userId, orderId, userEmail } = req.body;
// Proses checkout logic di sini
await processCheckout(userId, orderId);
// Push ke queue, jangan tunggu
await sendEmailWithPriority({
to: userEmail,
subject: 'Konfirmasi Pesanan #' + orderId,
html: generateOrderConfirmationHtml(orderId),
priority: 5,
});
res.json({ success: true, message: 'Checkout berhasil' });
});
Client langsung dapat response, email dikirim di background. Ini bikin API Anda tetap cepat meskipun volume email tinggi.
Panduan Praktis: Kode Lengkap
Berikut adalah struktur file yang recommended untuk project:
/src
/queues
email-queue.js # Setup queue dan worker
email-sender.js # Fungsi nodemailer
/routes
checkout.js # Route yang trigger email
app.js # Express app utama
Dengan arsitektur ini, Anda bisa scaling worker secara independently dari API server. Worker bisa dijalankan di dedicated server dengan spesifikasi yang sesuai untuk high-volume SMTP.
Kalau masalah bottleneck email di server sendiri yang jadi preocupação, KIRIM.EMAIL Dev menyediakan SMTP infrastructure yang sudah include queue management, monitoring, dan retry otomatis. Jadi Anda tidak perlu setup worker dan Redis sendiri. Cukup connect ke SMTP KIRIM.EMAIL dan biarkan mereka handle scaling-nya.
FAQ
Apa bedanya BullMQ dengan library queue Node.js lainnya?
BullMQ adalah pilihan paling matang untuk email queue di ekosistem Node.js karena beberapa alasan. Pertama, dokumentasi dan komunitas yang besar membuat troubleshooting lebih mudah. Kedua, fitur seperti priority queue, dead letter, dan rate limiter sudah built-in. Ketiga, BullMQ menggunakan Redis sebagai broker yang sudah teruji untuk high-throughput message passing. Alternatif seperti agenda menggunakan MongoDB sebagai broker, tapi MongoDB tidak secepat Redis untuk use case queue. kue adalah pilihan lama yang sudah tidak actively maintained.
Bagaimana cara scaling worker secara horizontal?
Worker BullMQ bisa dijalankan di multiple server karena semua state tersimpan di Redis. Cukup jalankan worker process yang sama di server berbeda, dan mereka akan secara otomatis load-balance job di antara mereka. Pastikan setiap worker punya unique worker ID untuk logging yang jelas. Untuk production dengan ribuan email per menit, 3-5 worker server biasanya sudah cukup.
Apakah Redis mandatory untuk BullMQ?
Ya, Redis adalah satu-satunya broker yang didukung BullMQ. Ini adalah design choice karena Redis menyediakan data structures yang tepat untuk queue (list, sorted set) dan sudah menjadi standar industri untuk caching dan message broker. Kalau Anda tidak ingin managed Redis sendiri, layanan seperti Upstash atau Redis Cloud menyediakan Redis dengan tier free yang cukup untuk development dan tier paid yang scalable untuk production.
Bagaimana cara monitoring queue health di production?
Ada beberapa pendekatan. Pertama, gunakan BullMQ dashboard yang sudah disebutkan di atas untuk monitoring manual. Kedua, integrasikan dengan Prometheus metrics untuk alerting otomatis. Ketiga, set up webhook atau event listener untuk setiap job completion dan failure, lalu kirim metrics ke sistem monitoring seperti Grafana atau Datadog. Yang paling penting: alert jika failed job count melebihi threshold tertentu, misalnya 1% dari total email yang dikirim.
Apakah queue in-memory (tanpa Redis) bisa dipakai?
Bisa, tapi tidak direkomendasikan untuk production. Queue in-memory seperti runq atau implementasi sendiri tidak bertahan jika server crash. Email yang sudah di-queue tapi belum diproses akan hilang. Redis menyediakan persistence option (RDB + AOF) yang memastikan job tidak hilang bahkan jika server mati. Untuk aplikasi critical seperti email transactional, persistence adalah keharusan.
Berapa concurrency yang tepat untuk email worker?
Tergantung pada SMTP provider Anda. Untuk shared SMTP seperti Gmail atau SMTP generik, mulai dari concurrency: 5 dengan limiter: { max: 10, duration: 1000 }. Untuk dedicated SMTP atau SMTP relay provider seperti KIRIM.EMAIL atau SendGrid, Anda bisa mulai dengan concurrency: 20-50. Selalu monitor response time SMTP dan adjust. Kalau SMTP response time mulai naik di atas 5 detik, turunkan concurrency.
Bagaimana cara handle email yang ingin di-schedule untuk waktu tertentu?
BullMQ mendukung delayed jobs dengan option delay. Contoh: emailQueue.add('send-email', data, { delay: 60000 }) akan menjalankan job 60 detik kemudian. Untuk use case seperti “kirim email reminder 1 jam sebelum appointment”, simpan job dengan delay yang sesuai. BullMQ menggunakan sorted set di Redis untuk menyimpan delayed jobs, jadi tidak ada masalah dengan memory mesmo untuk ribuan scheduled jobs.
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 Monitor Email Sent Logs dan Audit Trail di Sistem Transaksional untuk Debugging dan Compliance - April 29, 2026
- Cara Debug Email Sending dengan Email Leak Testing untuk Identifikasi Privacy Leak di Aplikasi Web - April 28, 2026
- Cara Setup Email Queue dengan Background Worker di Node.js untuk Aplikasi High-Volume - April 27, 2026
