karawaci.kode

2026-08-03 · 10 min

PgBouncer vs Supabase Pooler vs Prisma Accelerate: Connection Pooling Node Production

Setiap kali saya onboarding ke codebase Node.js baru yang sudah berjalan beberapa bulan di production, hampir selalu ada satu masalah yang sama: connection pooling belum dikonfigurasi dengan benar, dan tim tidak tahu persis kenapa query tiba-tiba lambat atau kenapa ada error “remaining connection slots are reserved” di jam sibuk. Artikel ini adalah catatan dari pengalaman menangani masalah itu di beberapa klien Jakarta — apa yang saya coba, apa yang berhasil, dan kapan masing-masing solusi masuk akal.

Kenapa connection pooling jadi masalah di Node.js

Node.js punya satu properti yang membuatnya berbeda dari runtime lain dalam konteks database: ia non-blocking dan bisa menjalankan ratusan operasi async secara bersamaan. Ini bagus untuk throughput, tapi artinya setiap instance aplikasi cenderung membuka banyak koneksi ke database sekaligus.

Postgres mengelola koneksi dengan model satu proses per koneksi. Bukan thread — proses. Setiap koneksi butuh fork proses baru, memori sendiri, dan overhead context-switching. Di server 4 vCPU, max_connections yang masuk akal adalah sekitar 100–150. Di atas itu, Postgres mulai kehabisan napas.

Masalahnya jadi eksponensial ketika ada auto-scaling atau deployment serverless. Tiga instance aplikasi dengan connectionLimit: 10 sudah berarti 30 koneksi. Naik ke 10 instance, langsung 100. Tambahkan deployment Vercel yang setiap function invocation bisa membuka koneksi baru, dan dalam hitungan detik database Anda kehabisan slot.

Tiga opsi yang ada di meja

PgBouncer — pooler klasik, self-managed

PgBouncer adalah pooler yang berjalan sebagai proses terpisah di depan Postgres. Aplikasi Anda konek ke PgBouncer (biasanya port 6432), bukan langsung ke Postgres. PgBouncer mengelola pool koneksi ke database yang sebenarnya.

Ada tiga mode operasi:

  • Session mode: satu koneksi client = satu koneksi server selama sesi. Hampir tidak membantu untuk masalah koneksi.
  • Transaction mode: koneksi server dipinjam hanya selama satu transaksi, lalu dikembalikan. Ini yang berguna.
  • Statement mode: terlalu restriktif untuk aplikasi biasa.

Untuk Node.js production, transaction mode adalah target. Konfigurasi dasar:

# /etc/pgbouncer/pgbouncer.ini
[databases]
myapp = host=127.0.0.1 port=5432 dbname=myapp

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt

# Jumlah koneksi aktual ke Postgres
pool_size = 25
# Jumlah maksimum koneksi dari semua client
max_client_conn = 1000
pool_mode = transaction

# Penting untuk health check
server_check_query = select 1
server_idle_timeout = 600
client_idle_timeout = 0

Di sisi aplikasi Node.js, tidak ada perubahan kode — cukup ganti host dan port ke PgBouncer. Tapi ada satu hal yang sering mengejutkan: prepared statements default dari driver node-postgres (pg) tidak bekerja di transaction mode. Harus dimatikan:

// Dengan pg langsung
const pool = new Pool({
  host: 'localhost',
  port: 6432,
  database: 'myapp',
  // prepared statements harus dimatikan
  // di pg, gunakan simple query protocol
});

// Eksplisit gunakan query biasa, bukan prepared statement
await pool.query({ text: 'SELECT * FROM users WHERE id = $1', values: [id] });

Dengan Prisma, ada flag khusus di connection string:

DATABASE_URL="postgresql://user:pass@localhost:6432/myapp?pgbouncer=true"

Flag ?pgbouncer=true memberitahu Prisma untuk menonaktifkan prepared statements dan menyesuaikan behavior-nya untuk transaction mode. Tanpa flag ini, Anda akan mendapat error aneh yang tidak jelas hubungannya dengan PgBouncer.

Kapan pakai PgBouncer: deployment on-premise atau VPS di mana Anda punya akses penuh ke infrastruktur. Sudah cukup untuk menangani ratusan koneksi dari beberapa instance aplikasi dengan Postgres di server yang sama atau berdekatan. Saya pakai ini di klien yang hosting di Hetzner — setup di Docker Compose bersama Postgres, jalan mulus.

# docker-compose.yml (potongan relevan)
services:
  pgbouncer:
    image: bitnami/pgbouncer:latest
    environment:
      POSTGRESQL_HOST: postgres
      POSTGRESQL_PORT: 5432
      PGBOUNCER_DATABASE: myapp
      PGBOUNCER_POOL_SIZE: 25
      PGBOUNCER_MAX_CLIENT_CONN: 500
      PGBOUNCER_POOL_MODE: transaction
    ports:
      - "6432:6432"
    depends_on:
      - postgres

Supabase Pooler — managed, built-in dengan platform

Kalau database Anda ada di Supabase, pooler sudah disediakan tanpa setup tambahan. Supabase menggunakan Supavisor (bukan PgBouncer — lebih lanjut soal ini di FAQ) yang bisa diakses lewat port 6543 versus port 5432 untuk koneksi langsung.

Di Prisma, perbedaannya hanya di connection string:

# Koneksi langsung (bypass pooler) — untuk migration
DIRECT_URL="postgresql://postgres.xxx:[email protected]:5432/postgres"

# Lewat Supabase Pooler — untuk runtime
DATABASE_URL="postgresql://postgres.xxx:[email protected]:6543/postgres?pgbouncer=true"

Di schema.prisma:

datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_URL")
}

Field directUrl penting: Prisma CLI (prisma migrate deploy, prisma db push) menggunakan koneksi langsung karena migration memerlukan session mode. Kalau tidak set ini, migration bisa gagal dengan error tidak informatif.

Kapan pakai Supabase Pooler: sudah pakai Supabase sebagai platform. Tidak ada alasan untuk setup PgBouncer sendiri — Supabase Pooler sudah cukup untuk mayoritas kasus. Limitasinya: Anda tidak bisa mengubah konfigurasi pool secara detail. pool_size ditentukan berdasarkan tier, dan max_client_conn juga punya batas per tier.

Satu hal yang sering diabaikan: di tier Free dan Pro, max_connections Postgres cukup kecil (sekitar 60 di Free, 200 di Pro). Kalau Anda langsung konek ke port 5432 tanpa pooler, slot ini cepat habis. Selalu gunakan port 6543.

Prisma Accelerate — edge caching + global pooling

Prisma Accelerate berbeda secara fundamental dari dua opsi sebelumnya. Ini bukan sekadar pooler — ia adalah proxy layer yang jalan di edge network Prisma (berbasis Cloudflare Workers), menawarkan:

  1. Connection pooling yang kompatibel penuh dengan semua fitur Prisma Client, termasuk prepared statements
  2. Query result caching dengan TTL
  3. Edge deployment — query dari function Vercel di US-East ke database Singapore bisa memanfaatkan cache di edge node terdekat

Setup-nya:

// Di Next.js / Vercel deployment
import { PrismaClient } from '@prisma/client'
import { withAccelerate } from '@prisma/extension-accelerate'

const prisma = new PrismaClient().$extends(withAccelerate())

// Query biasa — tanpa perubahan
const user = await prisma.user.findUnique({
  where: { id: userId }
})

// Query dengan caching
const popularProducts = await prisma.product.findMany({
  where: { featured: true },
  cacheStrategy: {
    ttl: 300,    // detik
    swr: 60,     // stale-while-revalidate
  }
})

Connection string cukup diganti ke URL Accelerate:

DATABASE_URL="prisma://accelerate.prisma-data.net/?api_key=YOUR_API_KEY"

Untuk migration, tetap butuh directUrl ke Postgres langsung.

Kapan pakai Prisma Accelerate: dua kasus utama. Pertama, deployment serverless yang banyak (Vercel, Cloudflare Workers) di mana setiap invocation membuka koneksi baru — ini masalah yang tidak bisa diselesaikan PgBouncer tanpa server tersendiri. Kedua, kalau database Anda ada di satu region tapi traffic datang dari banyak region, caching di edge Accelerate bisa memotong latency secara signifikan.

Ongkosnya: ada biaya per query di luar free tier, dan Anda memasukkan satu dependency pihak ketiga ke jalur kritial database. Kalau Accelerate down, aplikasi Anda down.

Perbandingan langsung

AspekPgBouncerSupabase PoolerPrisma Accelerate
SetupManual, self-managedZero-config (di Supabase)Via API key, managed
BiayaGratis, cuma infrastrukturTermasuk Supabase tier$0–berbayar per query
Transaction modeYaYa (Supavisor)Ya
Prepared statementsHarus dimatikanHarus dimatikanSupported penuh
Query cachingTidakTidakYa, dengan TTL
Edge/globalTidakTidakYa
Kontrol konfigurasiPenuhTerbatasTidak ada
Cocok untukVPS/on-premSupabase usersServerless/edge

Trade-off yang perlu jujur diakui

PgBouncer menambah satu hop di jalur query. Latency overhead biasanya < 1 ms di jaringan lokal, tapi ada. Lebih krusial: Anda harus mengelola prosesnya sendiri — monitoring, restart, upgrade. Di production saya pernah mengalami PgBouncer hang diam-diam tanpa error log yang jelas; perlu alert aktif di pg_stat_activity untuk mendeteksi.

Supabase Pooler nyaman tapi tidak transparan. Anda tidak bisa lihat atau ubah konfigurasi pool secara detail. Kalau ada masalah performa di level pooler, opsi Anda terbatas. Ini trade-off yang wajar jika Anda memilih managed platform.

Prisma Accelerate adalah abstraksi yang paling jauh dari database. Setiap query melewati server Prisma terlebih dahulu. Untuk query yang tidak di-cache, ini menambah latency dibanding koneksi langsung. Saya lihat angka tambahan 5–15 ms per query tergantung region — mungkin tidak signifikan untuk aplikasi dengan SLA detik, tapi terasa di aplikasi yang sensitif latency.

Satu hal yang tidak bisa dilakukan Accelerate: streaming besar via cursor, atau operasi yang butuh koneksi stateful seperti LISTEN/NOTIFY. Ini bukan solusi universal.

Verdict

Untuk VPS atau server yang Anda kontrol: pasang PgBouncer. Transaction mode, pool_size sekitar 20–30 per aplikasi, max_client_conn bisa ratusan. Ini paling murah dan paling fleksibel. Flag ?pgbouncer=true di Prisma connection string wajib ada.

Untuk Supabase: gunakan Supabase Pooler via port 6543, set directUrl untuk migration, dan selesai. Tidak perlu tambah layer lain kecuali ada kebutuhan spesifik.

Untuk Vercel atau Cloudflare Workers yang intensif: Prisma Accelerate masuk akal jika serverless cold start dengan banyak koneksi adalah masalah nyata yang Anda alami, atau kalau Anda butuh query caching tanpa setup Redis. Tapi kalau aplikasi Anda hanya punya beberapa function dan traffic tidak tinggi, biaya tambahannya belum tentu worth it dibanding PgBouncer di server kecil.

Yang jelas salah: tidak pakai pooling sama sekali di production dengan Prisma. Default Prisma membuka connection pool internal, tapi pool ini tidak membantu kalau Anda punya banyak instance atau serverless deployment. Di production kami di satu klien fintech Tangsel, berpindah dari koneksi langsung ke PgBouncer transaction mode mengurangi max_connections usage dari 95% ke sekitar 30% — tanpa perubahan kode aplikasi satu baris pun.

Pilih berdasarkan infrastruktur yang sudah ada, bukan berdasarkan mana yang paling canggih.

Ditulis oleh Reza Pradipta