API Route Next.js sering dipakai untuk men-trigger pekerjaan berat sambil melayani permintaan sinkron. Untuk menjaga konsistensi data dan menghindari duplikasi eksekusi, gunakan Redis sebagai cache/lock dan worker queue sebagai pemroses terpisah. Pendekatan ini memperkecil beban server Next.js dan memberi kontrol lebih baik terhadap latensi, retries, dan cache invalidation.

Artikel ini menunjukkan bagaimana menggabungkan cache Redis, locking/visibility timeout, serta worker queue agar API route tetap idempotent dan cache selalu merepresentasikan state yang valid tanpa menunggu job selesai.

Mengapa Redis Cache + Worker Queue Diperlukan di API Route Next.js

Next.js API Route ideal untuk menerima permintaan HTTP cepat, tetapi tidak untuk mengerjakan proses panjang. Mengembalikan respons sesegera mungkin dari cache mencegah timeout dan menjaga user experience. Redis cache menampung hasil terakhir, sementara worker queue menjalankan pekerjaan berat secara asinkron. Kombinasi keduanya membuat API tetap responsif dan data konsisten.

Redis juga menyediakan mekanisme locking ringan (SET NX + TTL) yang membantu mencegah duplikasi pemrosesan jika API dipanggil bersamaan. Worker queue yang di-backend oleh Redis (misalnya BullMQ atau Bee-Queue) mendukung visibilitas lock dan retry otomatis, memungkinkan kontrol penuh atas lifecycle job.

Arsitektur Konsistensi: API Route, Cache, Worker Queue

Berikut alur umum yang harus diikuti:

  • API Route memeriksa cache Redis untuk respons terakhir.
  • Jika cache valid, API langsung merespons tanpa memicu job.
  • Jika tidak ada cache atau indikator dirty, API mencoba membuat lock, lalu mendaftarkan job ke queue.
  • Worker queue memproses job, memperbarui data primer, lalu memvalidasi atau menghapus cache sesuai strategi.
  • Cache diberi TTL, dan job menonaktifkan lock setelah selesai sehingga permintaan baru bisa memicu pembaruan bila perlu.

Diagram alur

Client      API Route       Redis Cache        Worker Queue           Worker Redis Lock
    |            |                |                   |                      |
    |--> request |                |                   |                      |
    |            |--> check cache  |                   |                      |
    |            |<-- hit?        |                   |                      |
    |            |   yes -> resp   |                   |                      |
    |            |   no -> set lock|                   |                      |
    |            |--> enqueue job  |                   |                      |
    |            |                |------------------>|                      |
    |            |                |                   |--> pop job           |
    |            |                |                   |--> process + update   |
    |            |                |<------------------|   data + cache invalidate
    |            |                |                   |                      |

Implementasi Next.js API Route dengan Cache, Lock, dan Enqueue Job

Gunakan ioredis untuk operasi cache dan lock, serta library queue (misalnya BullMQ). API harus bersifat idempotent: periksa cache, buat lock yang memiliki TTL (misalnya 30 detik), dan enqueue job hanya bila lock berhasil.

import { NextApiRequest, NextApiResponse } from 'next';
import Redis from 'ioredis';
import { Queue } from 'bullmq';

const redis = new Redis(process.env.REDIS_URL);
const queue = new Queue('heavy-job', { connection: redis });

const CACHE_KEY = 'latest:order-status';
const LOCK_KEY = 'lock:order-status';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  // 1. Periksa cache
  const cached = await redis.get(CACHE_KEY);
  if (cached) {
    return res.status(200).json({ data: JSON.parse(cached), source: 'cache' });
  }

  // 2. Coba ambil lock sebelum enqueue job
  const lockAcquired = await redis.set(LOCK_KEY, 'locked', 'NX', 'EX', 30);
  if (!lockAcquired) {
    return res.status(202).json({ message: 'Proses sedang berjalan, coba sedikit lagi.' });
  }

  try {
    await queue.add('refresh-order', { userId: req.query.userId }, { jobId: `order:${req.query.userId}` });
    return res.status(202).json({ message: 'Job sedang diproses, hasil akan tersedia sesegera mungkin.' });
  } catch (error) {
    await redis.del(LOCK_KEY);
    return res.status(500).json({ error: 'Gagal enqueue job' });
  }
}

Job enqueuing menggunakan jobId berbasis user/order untuk menghindari duplikasi. Lock dilepas oleh worker setelah selesai. TTL lock menjamin fallback jika worker crash.

Worker Queue: Visibility Timeout, Retry, dan Konsistensi Cache

Worker harus mengambil job dari queue, melakukan proses (misalnya memanggil service eksternal), dan memperbarui cache setelah data tercommit. Atur visibility timeout dengan lockDuration atau job.lockDuration agar job yang gagal dapat diambil ulang setelah timeout.

import { Worker, Job } from 'bullmq';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);
const CACHE_KEY = 'latest:order-status';
const LOCK_KEY = 'lock:order-status';

const worker = new Worker('heavy-job', async (job: Job) => {
  const data = await fetchOrderFromService(job.data.userId);

  // 1. Simpan ke cache dengan TTL relatif pendek
  await redis.set(CACHE_KEY, JSON.stringify(data), 'EX', 60);
  // 2. Lepas lock agar API bisa enqueue job baru jika perlu
  await redis.del(LOCK_KEY);
  return data;
}, {
  connection: redis,
  lockDuration: 45000, // lock worker, mirip visibility timeout
  attempts: 3,
  backoff: { type: 'exponential', delay: 2000 }
});

worker.on('failed', async (job, err) => {
  console.error('Job gagal:', job.id, err.message);
  await redis.del(LOCK_KEY);
});

Lock lockDuration menentukan berapa lama job dianggap sedang diproses sebelum dianggap gagal, mirip visibility timeout pada sistem antrian lain. Worker menghapus lock walau job gagal agar tidak ada deadlock permanen.

Strategi Cache Invalidation dan Ketahanan

  • TTL konservatif: Cache tidak boleh bertahan terlalu lama. Gunakan TTL pendek (misalnya 60 detik) dan perbarui saat job selesai.
  • Dirty flag: Simpan flag tambahan saat job dijalankan, sehingga API tahu cache perlu di-refresh walau belum kadaluarsa.
  • Fallback langsung: Bila cache tidak ada dan lock tidak bisa diambil, responkan status 202 dengan pesan proses sedang berjalan.
  • Cache invalidation eksplisit: Setelah job berhasil, hapus key lama sebelum menyimpan data baru untuk menghindari race condition antara update cache dan pembacaan berikutnya.

Debugging Operasional: Job Gagal, Retry, Cache Stale

  • Job gagal: Periksa log worker untuk stack trace. Pastikan failed event logging dan exception stack muncul di monitoring. Kunci Redis harus dilepas di event ini untuk menghindari lock stuck.
  • Retry: Konfigurasi attempts + backoff memberikan ruang untuk transient failure. Jangan lupa memantau repeatable job agar tidak memicu duplikasi akibat timeout.
  • Cache stale: Petakan kasus di mana data cache terlalu lama (lihat TTL expirations) dan tambahkan health check job yang memaksa refresh jika job terakhir menempuh waktu lama.
  • Lock stuck: Bila API terus mengembalikan 202 walau worker sudah selesai, periksa TTL lock. Bisa jadi lock tidak dihapus karena exception; tambahkan cleanup di finally block dan event completed.

Kesimpulan

Dengan cache Redis yang diatur TTL, locking, dan worker queue yang memadai, API Route Next.js dapat mempertahankan konsistensi data tanpa mengorbankan responsivitas. Fokus pada deduplikasi job (jobId), visibility timeout/lock duration, dan penghapusan cache setelah job sukses memungkinkan sistem berperilaku deterministik.

Debugging menjadi lebih mudah bila logging job failed, status retry, serta cache expiry terlihat di observability stack. Pendekatan ini juga memberi fleksibilitas untuk menambahkan monitoring atau escalation otomatis bila queue menumpuk.