Memindahkan kerja berat ke background: memahami konsep queue, broker, dan worker, membangun background job dengan BullMQ dan Redis, menjadwalkan tugas berulang dengan cron, serta pola retry dan idempotency.

Setiap aplikasi akhirnya punya kerja yang terlalu lambat untuk dijalankan di dalam request: kirim email, proses gambar, sinkronisasi dengan API eksternal. Jika kalian menjalankannya secara sinkron, pengguna menunggu berdetik-detik — dan jika gagal di tengah, semua harus mulai ulang. Episode ini membahas background jobs & queues: memindahkan kerja berat keluar dari request pengguna.
Mengapa topik ini wajib untuk fullstack? Karena "fitur selesai" di dunia nyata hampir selalu berarti kerja latar: setelah user submit, ada serangkaian proses yang harus berjalan di belakang tanpa membuat user menunggu.
Bayangkan sebuah dapur restoran: pelanggan memesan (producer), pesanan diletakkan di papan jendela (broker/queue), dan koki mengambil pesanan satu per satu (worker). Ketika pelanggan selesai memesan, ia tidak menunggu di depan koki — ia kembali ke meja.
| Peran | Tugas | Contoh |
|---|---|---|
| Producer | Mengirim job ke queue | Server action setelah submit |
| Broker | Menyimpan & mengantre job | Redis + BullMQ |
| Worker | Memproses job dari queue | Proses terpisah, retry otomatis |
Keuntungan strukturalnya: server action selesai cepat (cukup menaruh job ke queue), dan pekerjaan diproses async dengan retry jika gagal.
Kita pakai BullMQ di atas Redis — pasangan paling umum untuk Node.js. Jalankan Redis via Docker (seperti PostgreSQL di episode 0):
docker run -d --name redis-fullstack -p 6379:6379 redis:7-alpinepnpm add bullmq ioredisimport { Queue, Worker } from "bullmq"
const connection = { host: "localhost", port: 6379 }
export const noteQueue = new Queue("notes", { connection })Dulu (sinkron): user menunggu SMTP yang lambat. Sekarang: producer menaruh job, worker yang mengirim.
Producer — dipanggil dari server action (tidak membuat user menunggu):
"use server"
import { noteQueue } from "@/lib/queue"
export async function createNote(formData: FormData) {
const note = await prisma.note.create({ data: {...} })
await noteQueue.add("send-email", {
noteId: note.id,
email: user.email,
})
revalidatePath("/notes")
}Worker — proses terpisah yang menjalankan job:
import { Worker } from "bullmq"
import { sendEmail } from "@/lib/email"
const worker = new Worker(
"notes",
async (job) => {
if (job.name === "send-email") {
await sendEmail({
to: job.data.email,
subject: "Catatan baru dibuat",
body: `Catatan ${job.data.noteId} berhasil disimpan`,
})
}
},
{ connection },
)Jalankan worker dengan perintah terpisah:
pnpm tsx src/worker/email.tsTip
Producer dan worker adalah proses terpisah — itulah intinya. Server action hanya "menaruh pesanan", worker "memasak" di prosesnya sendiri. Ini berarti aplikasi kalian bisa memproses banyak job bersamaan tanpa memperlambat response pengguna.
Kerja latar gagal — jaringan putus, API eksternal down. Queue dirancang untuk ini: BullMQ retry otomatis dengan backoff:
await noteQueue.add("send-email", { noteId }, {
attempts: 5, // coba 5 kali
backoff: { type: "exponential", delay: 2000 },
})Aturan penting untuk worker: idempotent — menjalankan job yang sama dua kali harus menghasilkan efek yang sama. Contoh: email yang gagal di retry tidak boleh terkirim ganda:
const sent = await prisma.emailLog.findUnique({ where: { noteId } })
if (sent) return // sudah terkirim, lewati
await sendEmail({ to, subject, body })
await prisma.emailLog.create({ data: { noteId } })Untuk tugas rutin (ringkasan harian, hapus data basi, backup), pakai cron. BullMQ menyediakan repeatable job:
await noteQueue.add("daily-summary", {}, {
repeat: { pattern: "0 6 * * *" },
})Sintaks cron "0 6 * * *" berarti "menit 0, jam 6, setiap hari". Jika butuh scheduler berbasis HTTP di Vercel/edge, episode 18 membahas alternatifnya (cron.json / edge cron).
Selesaikan dengan urutan berikut:
notes dengan satu job send-email (bisa pakai logger sebagai pengganti email asli).queue.add dari server action createNote.curl -s http://localhost:3000/notes/new -X POST ...
# di terminal worker:
# {"msg":"sending email for note ..."}Jika log worker muncul tanpa membuat halaman menunggu, arsitektur queue kalian bekerja.
Warning
Bahaya tersembunyi queue: job menumpuk tanpa dipantau. Selalu pasang alert saat panjang queue melonjak, dan batasi retry dengan attempts — job yang gagal total sebaiknya masuk "dead letter" untuk ditangani manual, bukan dicoba selamanya.
Inti yang harus dibawa pulang:
Di episode 14 selanjutnya kita membahas real-time features — WebSocket, SSE, dan live updates — untuk membangun fitur yang bereaksi tanpa refresh halaman. Sampai jumpa di episode 14!