Debugging API Route Next.js: Retry Gagal Karena Redis Lag di Produksi

Masalah utama yang kami hadapi adalah API route Next.js yang menolak menjawab karena retry otomatis terus berujung pada error meskipun klien sudah mengirim ulang permintaan. Penyebab langsungnya adalah Redis yang tiba-tiba mengalami latensi tinggi dan akhirnya menolak koneksi, sehingga retry tidak pernah me-recover. Artikel ini menunjukkan gejala produksi, cara reproduksi lokal, data observabilitas yang membantu, serta analisis kode hingga root cause dan solusi.

Gejala Produksi dan Analisis Awal

Gejala pertama terlihat dari dashboard pemantauan: beberapa endpoint API route kami mengalami lonjakan 500 dan response time bertahan di atas 1 detik sebelum gagal. Observasi log menyebutkan adanya retry dari middleware internal yang memanggil Redis, sementara trace dari distributed tracing menunjukkan bahwa permintaan menunggu lebih dari 5 detik di layer layanan cache.

Observabilitas: Log, Metrics, dan Trace

  • Log: setiap permintaan memunculkan log “Redis GET timeout” di handler, disusul error “Retry limit reached”.
  • Metric latency: histogram latency Redis menunjukkan bucket > 1s naik tajam pada interval kejadian.
  • Trace: span API route dipanjangkan karena middleware melihat retry_connection tiga kali sebelum menyerah; span Redis menunjukkan banyak fungsi menunggu status blocked.

Dengan data ini kita menyimpulkan bahwa retry tidak disebabkan oleh request data yang salah, melainkan karena ketergantungan Redis menunda respon tanpa timeout yang jelas.

Mengeksekusi Reproduksi Lokal

Reproduksi dilakukan dengan menjalankan Next.js secara lokal dan menyuntikkan latensi Redis menggunakan tc atau stub TCP untuk menunda respons. Langkahnya:

  1. Jalankan Redis di mesin lokal dan simulasikan lag dengan tc qdisc add dev lo root netem delay 500ms.
  2. Gunakan API route Next.js yang meminta data dari Redis dan kembali ke klien.
  3. Perhatikan bahwa retry internal klien Redis menunggu tanpa batas lalu melempar error setelah timeout default.

Perilaku ini merefleksikan produksi dan memberi lingkungan untuk menguji perbaikan.

Analisis Root Cause

Bagian inti API route yang bermasalah mengeksekusi kode berikut:

import { createClient } from '@redis/client';

const redisClient = createClient({
  url: process.env.REDIS_URL,
  socket: {
    connectTimeout: 10000,
    reconnectStrategy: retries => Math.min(retries * 100, 1000)
  }
});

export default async function handler(req, res) {
  try {
    await redisClient.connect();
    const value = await redisClient.get('user:session:' + req.query.id);
    if (!value) {
      throw new Error('tidak ada data');
    }
    res.status(200).json({ value });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

Di atas terlihat bahwa setiap request membuka koneksi dan memanggil get tanpa batasan timeout tambahan. Kesalahan utama adalah bergantung pada retry internal Redis yang hanya dilakukan setelah timeout panjang, ditambah tidak ada circuit breaker atau fallback ketika Redis lag.

Konfigurasi Timeout dan Retry

Retry client Redis tidak diatur untuk menyesuaikan dengan durasi API route. Akibatnya, saat Redis melambat, retry menumpuk dan akhirnya memblokir thread Node.js yang menjalankan handler. Ini menyebabkan permintaan baru ditolak sebelum mencapai fallback atau cache alternatif.

Redis Lag dan Connection Pool

Redis mengalami lag karena penggunaan koneksi burst dari route yang tidak menggunakan pooling tepat. Saat load meningkat, banyak koneksi terbuka dengan latensi tinggi; client memegangnya tanpa melepaskan dan akhirnya men-trigger timeout.

Perbaikan dan Mitigasi Praktis

Setelah mengidentifikasi root cause, tim melakukan langkah berikut:

  • Bounded timeout: menambahkan timeout eksplisit pada operasi Redis agar handler bisa mengembalikan error cepat. Contoh menggunakan Promise.race untuk menghindari plugin-specific features.
  • Fallback cache: sediakan cache lokal (misalnya Map berbasis TTL) untuk konten yang jarang berubah sehingga request tetap bisa dilayani meski Redis gagal sementara.
  • Circuit breaker: implementasikan pola circuit breaker dengan counter kegagalan; jika threshold tercapai, request langsung ke fallback tanpa berusaha lagi ke Redis hingga interval reset.

Contoh pengaturan timeout sederhana:

async function redisGetWithTimeout(key, timeoutMs = 500) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Redis timeout')), timeoutMs)
  );
  return Promise.race([redisClient.get(key), timeout]);
}

Prioritaskan fallback cache ketika kunci penting tersedia; jika tidak, berikan response 503 dengan alasan yang jelas agar upstream tidak terus retry.

Pelajaran untuk Developer Next.js

  • API route Next.js harus mengontrol ketergantungan non-deterministik seperti Redis; jangan berharap retry internal saja menyelesaikan kegagalan.
  • Observabilitas adalah kunci: log kesalahan timeout, metric latency, dan trace membantu memahami apakah masalah muncul dari Redis atau dari Node.js sendiri.
  • Gunakan circuit breaker dan fallback cache untuk memastikan kestabilan saat backend tidak responsif.
  • Reproduksi lokal dengan delay jaringan membantu memvalidasi perbaikan sebelum diterapkan ke produksi.

Dengan pendekatan ini, tim mampu menstabilkan API route, mengurangi error, dan menjadikan Redis lag sebagai kondisi yang bisa ditangani, bukan penghambat sistem.