Lonjakan trafik mendadak pada aplikasi Nuxt 3 sering mengungkap celah kritis pada manajemen koneksi database di layer server (Nitro). Gejala yang paling umum adalah lonjakan status HTTP 500 dengan pesan error seperti FATAL: remaining connection slots are reserved for non-replication superuser connections atau Connection terminated unexpectedly.

Root Cause: Instansiasi Client di Dalam Event Handler

Penyebab utama pool exhaustion pada runtime Nitro adalah pembuatan instance koneksi baru di dalam fungsi defineEventHandler. Berbeda dengan server Node.js monolitik tradisional yang menginisialisasi koneksi satu kali saat proses *bootstrapping*, handler Nitro dieksekusi secara independen setiap kali menerima request masuk.

Perhatikan pola anti-pattern berikut:

// server/api/users.get.ts - ANTI-PATTERN
import { Pool } from 'pg'

export default defineEventHandler(async (event) => {
  // Kesalahan: Pool baru dibuat pada setiap HTTP request
  const pool = new Pool({
    connectionString: process.env.DATABASE_URL
  })

  try {
    const { rows } = await pool.query('SELECT id, name FROM users')
    return rows
  } finally {
    // Menutup pool per request menambah latency tinggi (TCP handshake berulang)
    // Jika tidak ditutup, koneksi akan menggantung hingga DB kehabisan slot
  }
})

Jika sebuah pool dikonfigurasi dengan batas default 10 koneksi, maka 20 request konkuren akan memicu Nitro untuk membuka hingga 200 koneksi ke PostgreSQL. Hal ini langsung melampaui limit default PostgreSQL (biasanya 100 koneksi).

Alur Investigasi: Identifikasi Masalah via Database dan Log

Sebelum melakukan refactoring, validasi apakah koneksi memang terkuras akibat request HTTP yang menggantung.

1. Monitoring Koneksi Aktif di PostgreSQL

Jalankan query berikut di PostgreSQL untuk melihat jumlah dan status koneksi yang sedang berjalan:

SELECT state, count(*) 
FROM pg_stat_activity 
WHERE datname = current_database() 
GROUP BY state;

Jika koneksi berstatus idle terus meningkat seiring bertambahnya request tanpa pernah turun kembali ke angka dasar, ini mengonfirmasi adanya kebocoran koneksi (*connection leak*) dari aplikasi.

2. Memeriksa Nilai Limit Koneksi

SHOW max_connections;

Bandingkan output query tersebut dengan total koneksi yang tercatat di pg_stat_activity saat pengujian lokal atau staging.

Solusi: Inisialisasi Singleton via Nitro Plugin

Pendekatan yang benar adalah membuat satu instance *connection pool* saat server Nitro mulai berjalan (*lifecycle start*), lalu menyuntikkan (*inject*) pool tersebut ke dalam context request (event.context).

1. Buat Nitro Plugin

File di dalam direktori server/plugins/ akan otomatis dieksekusi sekali saat runtime server aktif.

// server/plugins/db.ts
import pg from 'pg'

const { Pool } = pg

export default defineNitroPlugin((nitroApp) => {
  const pool = new Pool({
    connectionString: process.env.DATABASE_URL,
    max: 20, // Batasi jumlah koneksi maksimal dalam pool
    idleTimeoutMillis: 30000,
    connectionTimeoutMillis: 2000
  })

  // Pasang pool ke context setiap request masuk
  nitroApp.hooks.hook('request', (event) => {
    event.context.db = pool
  })

  // Tutup pool secara bersih saat server shutdown
  nitroApp.hooks.hook('close', async () => {
    await pool.end()
  })
})

2. Konfigurasi Type Definition (TypeScript)

Agar event.context.db memiliki autocompletion dan tipe yang valid, tambahkan deklarasi modul berikut:

// server/types/nitro.d.ts
import type { Pool } from 'pg'

declare module 'h3' {
  interface H3EventContext {
    db: Pool
  }
}

3. Konsumsi Database Client di Handler

Gunakan pool yang sudah disediakan pada event.context tanpa menginisialisasi ulang driver:

// server/api/users.get.ts - PATTERN BENAR
export default defineEventHandler(async (event) => {
  const pool = event.context.db

  const { rows } = await pool.query(
    'SELECT id, name, created_at FROM users LIMIT 50'
  )

  return rows
})

Verifikasi Stabilitas Menggunakan Autocannon

Lakukan uji beban (*load test*) lokal menggunakan autocannon untuk membuktikan bahwa pool tetap stabil di bawah tekanan konkurensi tinggi.

Jalankan perintah berikut pada endpoint yang telah diperbaiki:

npx autocannon -c 50 -d 15 http://localhost:3000/api/users

Perintah ini menjalankan 50 concurrent connection selama 15 detik. Periksa kembali monitoring PostgreSQL selama tes berjalan:

SELECT count(*) FROM pg_stat_activity WHERE datname = current_database();

Hasil yang diharapkan:

  • Jumlah koneksi tidak akan melebihi parameter max: 20 yang telah dikonfigurasi di plugin.
  • Persentase error (HTTP 5xx) adalah 0%.
  • Latency tetap konsisten karena koneksi TCP digunakan kembali (*reused*) via pool.

Catatan Arsitektur: Node.js vs Serverless

Pola singleton plugin ini dirancang khusus untuk deployment berbasis Node.js standar (Docker container, VPS, PM2, atau Kubernetes). Jika aplikasi di-deploy ke platform Serverless / Edge (seperti Vercel Functions atau AWS Lambda), setiap fungsi berjalan di environment terisolasi dengan siklus hidup pendek.

Pada arsitektur serverless murni, gunakan connection proxy eksternal seperti PgBouncer, Supabase Connection Pooler (Supavisor), atau driver HTTP/WebSocket database (seperti Neon serverless driver) untuk mencegah kehabisan koneksi pada database engine.