Debugging Nuxt.js SSR API memerlukan pendekatan sistematis ketika cache middleware menyebabkan latency tinggi dan data yang tidak sinkron. Dalam studi kasus ini, kita langsung menanggapi permasalahan: API SSR mengembalikan data lama karena cache key tidak konsisten pada multi-instanse dan penanganan invalidasi yang terlewat, sehingga pengguna mengalami data tidak sinkron dan metrik latency melonjak.

Artikel ini menguraikan gejala yang terlihat, investigasi log dan konfigurasi cache, root cause yang ditemukan, serta langkah perbaikan berurutan hingga validasi pasca-perbaikan, lengkap dengan snippet konfigurasi dan verifikasi.

Gejala nyata pada API SSR yang menggunakan cache middleware

Dalam produksi, endpoint SSR /api/profile mulai menunjukkan perilaku tidak stabil: respon sering tertunda 400+ ms meski beban tetap, dan pengguna melihat data profil lama setelah melakukan update.

Log dari middleware cache menunjukkan banyak cache.hit meski payload terakhir di database berubah, serta spike pada counter cache miss yang tidak diikuti refresh. Gejala lain: latency spike hanya terjadi di request dengan header tertentu (user selain admin) karena key cache yang tergantung nilai header.

Contoh log yang menandai isu

[2024-10-12T09:21:03Z] WARN cache: cache.hit key=profile-user-42 value=stale
[2024-10-12T09:21:03Z] INFO api: downstream fetch duration=512ms
[2024-10-12T09:21:03Z] WARN cache: served stale object without refresh

Log semacam ini menunjukkan cache menyajikan data tapi tidak melakukan revalidation ketika backend mengeluarkan event data baru.

Langkah-langkah investigasi

1. Validasi log dan metrik

Aktifkan logging pada cache middleware untuk menyertakan key, ttl, dan status fetch. Perhatikan apakah cache miss disebabkan oleh race condition antar server. Gunakan query dari observability (contoh di Datadog):

avg:last_5m:cache.response_time{env:prod,status:miss} by {instance}

Jika response time miss tinggi dan bervariasi antar instans, kemungkinan ada penundaan pada fetching upstream.

2. Tinjau konfigurasi cache middleware

Periksa file middleware (misal server/middleware/cache.ts) untuk memastikan key deterministik dan TTL sesuai kebutuhan cacheable data. Versi awal middleware terlihat seperti ini:

export default defineEventHandler(async (event) => {
  const key = `profile-${event.req.headers['x-user-id']}`
  const cached = await cacheStore.get(key)
  if (cached) {
    return cached
  }

  const response = await $fetch('/api/internal/profile', {
    headers: { authorization: event.req.headers.authorization }
  })
  await cacheStore.set(key, response, 300)
  return response
})

Masalah: penggunaan header langsung tanpa fallback menyebabkan key berbeda jika header tidak ada atau berisi spasi, sehingga instans menganggap cache miss dan menulis kembali, menciptakan race write yang tidak sinkron.

Root cause: race condition dan cache key tidak deterministik

Selama load tinggi, dua request pertama membentuk key yang berbeda karena header x-user-id kosong, lalu menulis cache sekaligus tanpa mekanisme lock. Akhirnya respon yang disajikan berasal dari instance pertama yang menulis, sementara instance lain memecahkan cache untuk TTL lama. Gabungan ini memunculkan data stale dan latency karena fetching bersamaan.

Karena middleware tidak melakukan validation terhadap isi respon upstream (misalnya etag atau timestamp), proses invalidasi manual tidak pernah terjadi.

Perbaikan konkret

  1. Standardisasi cache key: Gunakan fallback default pada middleware.

    const userId = (event.req.headers['x-user-id'] ?? 'anonymous').trim()
    const key = `profile-${userId}`
  2. Gunakan locking sederhana untuk menghindari race condition saat cache miss.

    if (!cached) {
      const lockKey = `lock-${key}`
      const acquired = await cacheStore.acquireLock(lockKey, 500)
      if (acquired) {
        const fresh = await fetchProfile()
        await cacheStore.set(key, fresh, 300)
        await cacheStore.releaseLock(lockKey)
        return fresh
      }
      return await cacheStore.get(key) // baca ulang setelah lock dilepas
    }
  3. Tambahkan metadata revalidation seperti lastFetched untuk memaksa invalidasi apabila backend mengeluarkan event profile.updated.

Perbaikan ini mengatasi dua kerawanan: key deterministik mencegah instans berbeda menulis entry berbeda, dan lock memastikan hanya satu fetch upstream yang dijalankan saat cache kosong.

Monitoring dan rollback

Sekarang kombinasikan metrik cache hit/miss dengan alert latency. Tambahkan dashboard berisi:

  • Rasio cache hit untuk /api/profile
  • Histogram latency SSR setelah cache hit dan miss
  • Jumlah lock contention (jika tersedia) pada mesin middleware

Jika perbaikan langsung menghasilkan peningkatan, rollout gradien dengan feature flag CACHE_V2_ENABLED. Jika ada regresi, lakukan rollback dengan menonaktifkan flag dan revert config sebelum melakukan analisis kembali.

Validasi pasca perbaikan

Lakukan pengujian manual bersama automated test:

  • Trigger update data profil lewat API admin, lalu panggil SSR page dalam waktu 1-2 detik untuk memastikan cache invalidasi.
  • Gunakan script load testing (misalnya k6) mengirim 200 concurrent request: semua harus menyelesaikan tanpa cache miss abnormal.
  • Periksa log dari middleware untuk memastikan tidak ada pesan cache.hit stash stale.

Contoh verifikasi sederhana:

$ curl -H "x-user-id: 42" https://example.com/profile
# pastikan respon terbaru dan pastikan header cache tertera ttl=300

Dengan pendekatan ini, Nuxt.js SSR API kembali stabil, cache middleware berfungsi tanpa menyajikan data usang, dan tim dapat memantau serta rollback perubahan jika dibutuhkan.