Belajar ExpressJS - Background Jobs & Queue
Episode 22 of 28

Belajar ExpressJS - Background Jobs & Queue

Memindahkan pekerjaan berat keluar dari request handler: BullMQ dengan Redis untuk job queue, worker process terpisah, retry dan error handling, serta cron untuk pekerjaan terjadwal.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

Sejauh ini semua pekerjaan berjalan di dalam request handler — dari membaca database sampai mengirim response. Episode 22 memperkenalkan pemisahan penting: pekerjaan yang tidak boleh memblokir request. Kirim email, generate laporan PDF, resize gambar, sinkronisasi dengan layanan lain — semuanya butuh waktu dan tidak perlu membuat client menunggu.

Mengapa queue, bukan sekadar Promise fire-and-forget? Karena pekerjaan latar belakang butuh tiga hal yang tidak dimiliki Promise biasa: persistensi (pekerjaan tidak hilang saat server restart), retry (gagal otomatis diulang), dan ketersediaan antar instance. BullMQ di atas Redis menyediakan semuanya — dan Redis sudah kalian miliki dari episode 20-21.

Konsep: Producer, Queue, Worker

100%

Tiga komponen dengan peran berbeda:

  • Producer — kode Express yang menambahkan job ke queue (queue.add).
  • Queue — Redis sebagai antrian persisten; job menunggu di sini sampai ada worker.
  • Worker — proses terpisah yang mengambil dan memproses job; bisa banyak instance paralel.

Pemisahan producer dan worker membuat request handler tetap cepat — ia hanya menambahkan job dan langsung mengembalikan response.

Setup BullMQ

Install BullMQ
npm install bullmq

Producer di Aplikasi Express

JSsrc/queues/emailQueue.js - producer
import { Queue } from "bullmq"
import { config } from "../config.js"
 
export const emailQueue = new Queue("email", {
  connection: { url: config.redisUrl },
})
 
export const sendEmailJob = async (to, subject, html) => {
  await emailQueue.add(
    "send-email",
    { to, subject, html },
    {
      attempts: 3,
      backoff: { type: "exponential", delay: 5000 },
    }
  )
}

Dua opsi job yang menentukan ketahanan:

  • attempts: 3 — job dicoba ulang hingga 3 kali saat gagal.
  • backoff — jeda antar percobaan makin lama (5s, 10s, ...) agar layanan yang error sempat pulih.

Memakai Producer di Handler

JSHandler yang mengantre email
import { sendEmailJob } from "../queues/emailQueue.js"
 
export const register = async (req, res) => {
  const user = await User.create(req.body)
 
  await sendEmailJob(user.email, "Selamat datang!", "<p>Terima kasih...</p>")
 
  res.status(201).json({ data: user })
}

Perhatikan: handler mengembalikan 201 segera — email dikirim asinkron oleh worker. Latensi request tidak tersentuh pekerjaan yang butuh detik.

Worker: Proses Terpisah

Worker berjalan di proses sendiri (file entry point terpisah), sehingga crash di worker tidak memengaruhi server HTTP:

JSsrc/workers/emailWorker.js
import { Worker } from "bullmq"
import { config } from "../config.js"
 
const worker = new Worker(
  "email",
  async (job) => {
    const { to, subject, html } = job.data
 
    await sendMail({ to, subject, html })
    console.log(`email ${job.id} terkirim ke ${to}`)
  },
  { connection: { url: config.redisUrl }, concurrency: 5 }
)
 
worker.on("completed", (job) => {
  console.log(`job ${job.id} selesai`)
})
 
worker.on("failed", (job, err) => {
  console.error(`job ${job.id} gagal: ${err.message}`)
})
 
console.log("email worker siap")
  • concurrency: 5 — worker memproses 5 job sekaligus (tidak berurutan).
  • Event completed/failed memberi visibilitas (dipasang ke logging episode 17 di produksi).

Menjalankan Worker

Script npm untuk worker
{
  "scripts": {
    "dev": "node --watch src/server.js",
    "dev:worker": "node --watch src/workers/emailWorker.js",
    "start:worker": "node src/workers/emailWorker.js"
  }
}

Di produksi, worker dijalankan sebagai proses terpisah (PM2 — episode 24). Karena worker stateless (hanya bergantung pada job di Redis), menambah instance worker = menambah paralelisme.

Note

Memisahkan worker ke proses berbeda adalah keputusan arsitektur penting: request handler tidak pernah terblokir pekerjaan berat, dan skala worker bisa dinaikkan/turunkan terpisah dari skala server HTTP. Ini pola dasar yang sama di balik semua sistem job modern.

Retry dan Error Handling

Kapan Job Gagal Otomatis Diulang

BullMQ memisahkan dua jenis kegagalan:

  • Gagal permanen (misal email tidak valid) — tidak berguna diulang. Lempar error khusus dan tetapkan attempts: 1.
  • Gagal sementara (layanan email down, rate limit) — diulang dengan backoff.
JSError permanen vs sementara
export const EMAIL_VALIDATION = "EMAIL_VALIDATION"
 
worker = new Worker(
  "email",
  async (job) => {
    const { to, html } = job.data
 
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(to)) {
      const err = new Error("Email tidak valid")
      err.name = EMAIL_VALIDATION
      throw err
    }
 
    await sendMail({ to, html })
  },
  {
    connection: { url: config.redisUrl },
    removeOnFail: { count: 100 },
  }
)

Perhatikan removeOnFail: { count: 100 } — BullMQ default menyimpan seluruh job gagal; tanpa batas, Redis penuh oleh job yang gagal. Atur batas retensi.

Cron dan Job Terjadwal

Repeatable Jobs

Beberapa pekerjaan berjalan terjadwal — ringkasan harian, pembersihan data, pembuatan laporan. BullMQ mendukung repeatable job:

JSJob terjadwal dengan BullMQ
import { Queue } from "bullmq"
import { config } from "../config.js"
 
export const reportQueue = new Queue("report", {
  connection: { url: config.redisUrl },
})
 
export const scheduleDailyReport = async () => {
  await reportQueue.add(
    "daily-report",
    { date: new Date().toISOString().slice(0, 10) },
    {
      repeat: { pattern: "0 6 * * *" },
      jobId: "daily-report",
    }
  )
}

pattern: "0 6 * * *" adalah cron — setiap hari pukul 06:00. jobId unik mencegah duplikasi (menambahkan job dengan id yang sama = update, bukan duplikat).

Validasi Job Data

Data job adalah input seperti halnya body request — validasi dengan zod (episode 13) di dalam worker:

JSValidasi data job di worker
import { z } from "zod"
 
const reportSchema = z.object({
  date: z.string().date(),
  scope: z.enum(["daily", "weekly"]).default("daily"),
})
 
async function processReport(job) {
  const { date, scope } = reportSchema.parse(job.data)
  // ...
}

Worker yang memvalidasi inputnya sendiri mencegah job rusak memproses data tidak terduga — dan menghentikan lebih awal, bukan gagal di tengah jalan.

Warning

Jangan pernah menjalankan pekerjaan berat langsung di dalam handler hanya karena "sementara". Thread utama Node.js adalah single-threaded untuk JavaScript — satu job sinkron yang lambat akan menunda semua request lain. Queue memindahkan beban ini ke proses yang tepat.

Common Pitfalls

Fire-and-Forget Tanpa Redis

Promise yang tidak di-await bisa hilang saat proses restart. Queue menyimpan job di Redis — restart server tidak kehilangan pekerjaan.

Worker dalam Proses HTTP

Menjalankan worker di server.js membuat server HTTP dan worker berebut resource. Pisahkan proses (file terpisah, PM2 terpisah).

Redis Menumpuk Job Gagal

Job gagal tanpa removeOnFail memenuhi Redis selamanya. Batasi retensi dan pantau queue (Bull Board / metrics episode 25).

Cron Ganda karena Multi Instance

Repeatable job yang ditambahkan dari banyak instance bisa terdaftar banyak kali — jobId unik mencegah ini. Selalu set jobId untuk repeatable job.

Penutup

Episode 22 memindahkan pekerjaan berat keluar dari request: BullMQ dengan Redis sebagai antrian persisten, producer Express yang mengantre job, worker proses terpisah dengan concurrency, retry dengan backoff, dan cron untuk pekerjaan terjadwal.

Inti yang harus dibawa pulang:

  • Queue = persistensi + retry + skalabilitas; Promise biasa tidak punya semuanya.
  • Producer (queue.add) memisahkan request handler dari pekerjaan berat.
  • Worker berjalan di proses terpisah dengan concurrency.
  • Bedakan gagal permanen (no retry) dan sementara (retry + backoff).
  • removeOnFail mencegah Redis penuh job gagal.
  • Repeatable job pakai jobId unik agar tidak terduplikasi lintas instance.

Di episode 23 selanjutnya kita akan menyempurnakan API list: pagination, filtering & versioning — limit/offset dan cursor, filter dan sort, serta API versioning yang benar. Sampai jumpa di episode 23!