Penggunaan BullMQ di Next.js untuk queue dan scheduler cocok saat aplikasi Anda perlu memproses pekerjaan di belakang layar tanpa memperlambat request utama. Contoh paling umum adalah mengirim email, membuat laporan, sinkronisasi data, atau menjalankan tugas terjadwal seperti pembersihan data dan rekap harian.

Intinya, Next.js sebaiknya tidak memproses job berat langsung di dalam request. Sebagai gantinya, request hanya menambahkan job ke queue, lalu worker terpisah memproses job tersebut. Untuk scheduler, BullMQ juga bisa dipakai untuk membuat job berulang dengan pola cron, selama ada proses worker dan koneksi Redis yang aktif.

Kapan BullMQ tepat digunakan di Next.js?

BullMQ adalah library queue berbasis Redis untuk Node.js. Di aplikasi Next.js, BullMQ berguna ketika Anda ingin memisahkan pekerjaan yang tidak perlu selesai saat itu juga dari alur HTTP utama.

Contoh kasus sederhana

  • Queue: user submit form, lalu sistem mengirim email notifikasi di background.
  • Queue: setelah transaksi berhasil, sistem membuat invoice PDF tanpa membuat user menunggu.
  • Scheduler: setiap jam sistem menghapus token yang kedaluwarsa.
  • Scheduler: setiap pagi sistem membuat rekap order harian.

Kenapa tidak langsung diproses di API route?

Kalau job diproses langsung di API route atau server action, request akan lebih lambat dan lebih rentan timeout. Selain itu, jika proses gagal di tengah jalan, Anda perlu membuat mekanisme retry sendiri. Dengan BullMQ, Anda mendapat antrian, retry, delay, dan pengaturan concurrency yang jauh lebih rapi.

Catatan penting: untuk production, worker BullMQ sebaiknya dijalankan sebagai proses terpisah dari web server Next.js. Ini lebih stabil, lebih mudah diskalakan, dan tidak bergantung pada lifecycle request.

Arsitektur dasar BullMQ di Next.js

Pola yang umum dipakai adalah sebagai berikut:

  1. Aplikasi Next.js menerima request dari user.
  2. Next.js menambahkan job ke Redis melalui queue.
  3. Worker terpisah membaca job dari Redis dan memprosesnya.
  4. Jika perlu job terjadwal, aplikasi atau worker mendaftarkan repeatable job dengan pola cron.

Komponen yang biasanya dipisahkan

  • Producer: kode yang menambahkan job ke queue.
  • Worker: proses yang menjalankan job.
  • Redis: penyimpanan antrian dan state job.

Pemisahan ini penting karena Next.js fokus menangani HTTP, sedangkan worker fokus memproses background task.

Struktur project yang disarankan

Struktur tidak harus persis seperti ini, tetapi pola berikut cukup aman untuk dipelihara:

src/
├── app/
│   └── api/
│       └── jobs/
│           └── email/
│               └── route.ts
├── lib/
│   └── bullmq/
│       ├── connection.ts
│       ├── queues.ts
│       └── scheduler.ts
└── workers/
    └── email.worker.ts

Install package

npm install bullmq ioredis

ioredis umum dipakai karena BullMQ membutuhkan koneksi Redis yang stabil dan mendukung fitur yang diperlukan.

Membuat koneksi Redis dan queue

Langkah pertama adalah membuat koneksi Redis yang dapat dipakai bersama.

// src/lib/bullmq/connection.ts
import IORedis from 'ioredis'

export const connection = new IORedis({
  host: process.env.REDIS_HOST || 'localhost',
  port: Number(process.env.REDIS_PORT || 6379),
  maxRetriesPerRequest: null,
})

Opsi maxRetriesPerRequest: null sering dipakai bersama BullMQ untuk menghindari masalah pada worker atau operasi queue tertentu yang membutuhkan perilaku koneksi lebih konsisten.

Lalu buat queue:

// src/lib/bullmq/queues.ts
import { Queue } from 'bullmq'
import { connection } from './connection'

export const emailQueue = new Queue('emailQueue', {
  connection,
  defaultJobOptions: {
    attempts: 3,
    backoff: {
      type: 'exponential',
      delay: 5000,
    },
    removeOnComplete: true,
    removeOnFail: false,
  },
})

Di sini kita memberi retry maksimal 3 kali dengan exponential backoff. Itu berguna untuk error sementara, misalnya layanan email sedang lambat.

Menambahkan job dari Next.js API route

Sekarang kita buat endpoint sederhana untuk menambahkan job kirim email.

// src/app/api/jobs/email/route.ts
import { NextResponse } from 'next/server'
import { emailQueue } from '@/lib/bullmq/queues'

export async function POST(req: Request) {
  try {
    const body = await req.json()
    const { to, subject } = body

    if (!to || !subject) {
      return NextResponse.json(
        { error: 'to dan subject wajib diisi' },
        { status: 400 }
      )
    }

    const job = await emailQueue.add('sendEmail', {
      to,
      subject,
    })

    return NextResponse.json({
      message: 'Job berhasil dimasukkan ke queue',
      jobId: job.id,
    })
  } catch (error) {
    return NextResponse.json(
      { error: 'Gagal membuat job' },
      { status: 500 }
    )
  }
}

Endpoint ini tidak benar-benar mengirim email. Ia hanya mendaftarkan job. Itu yang membuat response tetap cepat.

Membuat worker BullMQ

Worker sebaiknya dijalankan sebagai proses Node terpisah. Jangan mengandalkan API route Next.js untuk memproses queue secara langsung, terutama jika aplikasi berjalan di environment yang mudah melakukan restart atau scale in/out.

// src/workers/email.worker.ts
import { Worker } from 'bullmq'
import { connection } from '@/lib/bullmq/connection'

const worker = new Worker(
  'emailQueue',
  async (job) => {
    if (job.name === 'sendEmail') {
      const { to, subject } = job.data

      console.log(`Mengirim email ke ${to} dengan subject: ${subject}`)

      // Simulasi proses kirim email
      await new Promise((resolve) => setTimeout(resolve, 1000))

      return { success: true }
    }
  },
  {
    connection,
    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)
})

Concurrency menentukan berapa banyak job yang diproses paralel oleh satu worker. Nilainya jangan langsung dibuat besar. Sesuaikan dengan beban CPU, I/O, dan karakter job Anda.

Script untuk menjalankan worker

// package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "worker:email": "tsx src/workers/email.worker.ts"
  }
}

Jika Anda memakai TypeScript untuk menjalankan file worker langsung, Anda bisa menggunakan alat seperti tsx. Yang penting idenya adalah worker berjalan sebagai proses terpisah.

Membuat scheduler atau cron job dengan BullMQ

Selain queue biasa, BullMQ juga bisa dipakai untuk job terjadwal. Misalnya, Anda ingin membersihkan data sementara setiap 5 menit atau menjalankan rekap setiap hari.

Pendekatan umumnya adalah menambahkan repeatable job ke queue dengan pola cron. Setelah terdaftar, worker akan memproses job tersebut sesuai jadwal.

// src/lib/bullmq/scheduler.ts
import { emailQueue } from './queues'

export async function registerSchedulers() {
  await emailQueue.add(
    'dailyReport',
    { type: 'daily-report' },
    {
      repeat: {
        pattern: '0 7 * * *',
      },
      jobId: 'daily-report-job',
    }
  )
}

Contoh di atas menjadwalkan job setiap pukul 07:00. Gunakan jobId yang konsisten agar Anda tidak tanpa sengaja membuat duplikasi scheduler saat proses inisialisasi dijalankan berkali-kali.

Menangani job scheduler di worker

// potongan di src/workers/email.worker.ts
import { Worker } from 'bullmq'
import { connection } from '@/lib/bullmq/connection'

const worker = new Worker(
  'emailQueue',
  async (job) => {
    switch (job.name) {
      case 'sendEmail': {
        const { to, subject } = job.data
        console.log(`Mengirim email ke ${to} dengan subject: ${subject}`)
        return { success: true }
      }
      case 'dailyReport': {
        console.log('Menjalankan rekap harian')
        // Simulasi generate report
        return { generated: true }
      }
      default:
        throw new Error(`Job tidak dikenali: ${job.name}`)
    }
  },
  { connection }
)

Kapan scheduler didaftarkan?

Idealnya, scheduler didaftarkan saat aplikasi atau service khusus startup. Yang penting, proses pendaftaran tidak membuat duplicate job yang tidak diinginkan. Karena itu, gunakan nama job dan jobId yang stabil, lalu lakukan pendaftaran dari satu tempat yang jelas.

Praktik yang aman: buat file inisialisasi scheduler terpisah dan jalankan sekali saat container worker atau app start. Hindari mendaftarkan scheduler di setiap request API.

Contoh use case sederhana: email queue dan cleanup scheduler

Supaya lebih konkret, berikut dua skenario yang sering dipakai sebagai awal implementasi:

1. Queue kirim email setelah user mendaftar

  • User mengirim request registrasi.
  • Data user disimpan ke database.
  • API menambahkan job sendEmail ke queue.
  • Worker memproses email verifikasi di background.

Keuntungan utamanya: endpoint registrasi tetap responsif meskipun layanan email lambat.

2. Scheduler membersihkan data kadaluarsa

  • Setiap 30 menit, sistem menjalankan job cleanup.
  • Worker menghapus token reset password yang sudah expired.
  • Operasi ini tidak perlu dipicu oleh user.

Pola ini lebih rapi dibanding menaruh logika cleanup di request acak yang tidak selalu terpanggil.

Konfigurasi Docker Compose untuk Next.js, Redis, dan worker

Untuk local development, Docker Compose memudahkan Anda menjalankan Redis bersama aplikasi Next.js dan worker BullMQ.

version: '3.9'
services:
  redis:
    image: redis:7-alpine
    ports:
      - '6379:6379'
    volumes:
      - redis_data:/data

  web:
    build:
      context: .
    command: npm run dev
    ports:
      - '3000:3000'
    environment:
      REDIS_HOST: redis
      REDIS_PORT: 6379
    depends_on:
      - redis
    volumes:
      - .:/app
      - /app/node_modules

  worker:
    build:
      context: .
    command: npm run worker:email
    environment:
      REDIS_HOST: redis
      REDIS_PORT: 6379
    depends_on:
      - redis
    volumes:
      - .:/app
      - /app/node_modules

volumes:
  redis_data:

Penjelasan singkat:

  • redis: menyimpan queue dan state job.
  • web: menjalankan Next.js.
  • worker: menjalankan proses BullMQ worker.

Contoh Dockerfile sederhana

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 3000

Untuk development, contoh ini cukup. Untuk production, biasanya Anda ingin image yang lebih efisien, build terpisah, dependency production-only, dan command yang lebih ketat.

Hal yang sering salah saat memakai BullMQ di Next.js

1. Worker dijalankan di dalam request handler

Ini kesalahan yang cukup umum. Queue producer boleh dipanggil dari API route, tetapi worker harus menjadi proses yang hidup sendiri. Jika tidak, perilakunya sulit diprediksi dan boros resource.

2. Scheduler didaftarkan berulang-ulang

Kalau setiap request atau setiap hot reload mendaftarkan repeatable job baru tanpa kontrol, Anda bisa mendapat job ganda. Gunakan ID yang stabil dan tempat inisialisasi yang jelas.

3. Tidak mengatur retry dan cleanup job

Kalau removeOnComplete tidak diatur, data job selesai bisa menumpuk di Redis. Sebaliknya, kalau semua job gagal langsung dihapus, Anda kehilangan bahan debugging. Atur sesuai kebutuhan operasional.

4. Memakai queue untuk pekerjaan yang sebenarnya sinkron dan ringan

Tidak semua hal perlu queue. Kalau prosesnya sangat cepat, deterministik, dan memang harus selesai sebelum response dikirim, queue justru menambah kompleksitas.

Tips debugging BullMQ

  • Cek koneksi Redis lebih dulu jika job tidak masuk atau worker tidak membaca job.
  • Log event completed dan failed pada worker untuk melihat alur proses.
  • Pastikan nama queue konsisten antara producer dan worker.
  • Uji job manual dengan endpoint sederhana sebelum menambahkan scheduler.
  • Perhatikan environment variable di container web dan worker; sering kali keduanya tidak memakai konfigurasi Redis yang sama.

Jika scheduler tidak jalan, periksa apakah job benar-benar sudah terdaftar, worker sedang hidup, dan pola cron sesuai timezone yang Anda harapkan. Masalah timezone cukup sering muncul pada job terjadwal.

Trade-off dan batasan

BullMQ sangat berguna, tetapi ada beberapa konsekuensi teknis:

  • Menambah komponen operasional: Anda perlu Redis dan worker terpisah.
  • Kompleksitas deployment meningkat: web app dan worker perlu dikelola bersama.
  • Observability perlu dipikirkan: log, retry, dan status job harus mudah dilacak.

Meski begitu, untuk aplikasi yang mulai memiliki banyak pekerjaan background atau job terjadwal, pemisahan ini biasanya sepadan karena arsitektur menjadi lebih stabil dan mudah dikembangkan.

Kesimpulan

Penggunaan BullMQ di Next.js untuk queue dan scheduler paling efektif ketika Anda memisahkan request web dari proses background. Next.js berperan sebagai producer yang menambahkan job ke Redis, sementara worker terpisah menjalankan job tersebut. Untuk cron atau scheduler, gunakan repeatable job dan pastikan pendaftarannya dilakukan secara terkontrol.

Mulailah dari use case sederhana seperti kirim email atau cleanup data, lalu tambah retry, logging, dan concurrency sesuai kebutuhan. Dengan pola ini, aplikasi Next.js Anda akan lebih responsif, lebih mudah diskalakan, dan lebih aman untuk menangani proses yang tidak cocok dijalankan langsung di request utama.