
Connection Pooling: Kenapa Aplikasi Backend Kamu Sering Kehabisan Koneksi Database
Error 'too many connections' di production hampir selalu soal connection pooling yang salah setup. Ini pengalaman saya mendiagnosis dan memperbaikinya di aplikasi Node.js + PostgreSQL.
Rabu malam, alert PagerDuty bunyi. Aplikasi SaaS multi-tenant yang saya pegang tiba-tiba mengembalikan error too many connections untuk role database. Aplikasi tidak ada deploy baru, traffic naik cuma 20% dari biasanya. Tapi koneksi ke PostgreSQL menumpuk sampai 300+, padahal max_connections kita set 200. Malam itu saya belajar satu hal dengan cara yang paling tidak menyenangkan: kebanyakan masalah koneksi database bukan karena database-nya lemah, tapi karena cara aplikasi kita mengelola connection pooling.
Di artikel ini saya mau bedah apa yang sebenarnya terjadi, kenapa masalah ini sering muncul di aplikasi Node.js/Prisma, dan angka-angka konkret yang saya pakai sekarang supaya insiden serupa tidak terulang.
Apa yang Sebenarnya Terjadi Saat Aplikasi "Kehabisan" Koneksi
Setiap koneksi ke PostgreSQL itu mahal. Database harus melakukan fork process baru (ya, PostgreSQL berbasis process, bukan thread), mengalokasikan memori, dan mempertahankan state itu selama koneksi hidup. Nilai max_connections default PostgreSQL cuma 100 — artinya kalau kamu tidak mengubah apa pun, seluruh aplikasi, cron job, dan DBA yang lagi buka psql berbagi kuota 100 itu.
Masalahnya, tanpa pooling, pola kode yang paling umum di Node.js justru membuka koneksi seketika dan membuangnya:
// ANTI-POLA: satu koneksi baru per request
app.get('/orders', async (req, res) => {
const client = new Client({ connectionString: process.env.DATABASE_URL });
await client.connect(); // mahal: TCP handshake + auth + fork process
const result = await client.query('SELECT * FROM orders WHERE tenant_id = $1', [req.tenantId]);
await client.end();
res.json(result.rows);
});
Di load rendah ini masih jalan. Tapi begitu ada 50 request konkuren, kamu punya 50 koneksi baru yang saling berebut. Tambah lagi beberapa instance aplikasi di belakang load balancer — misal 4 pod × 50 koneksi = 200 koneksi — dan max_connections langsung jebol. Persis skenario insiden saya.

Connection Pooling Itu Sebenarnya Apa
Connection pool itu kumpulan koneksi yang dibuka sekali di awal, lalu dipinjam dan dikembalikan oleh request. Tidak ada TCP handshake dan autentikasi berulang. Di ekosistem Node.js, kamu hampir selalu sudah memakai pooling tanpa sadar: pg punya Pool, Prisma punya connection pool internal di @prisma/client, dan pgBouncer bekerja di level antara aplikasi dan database.
Kuncinya bukan "pakai pool atau tidak", tapi berapa ukuran pool yang tepat dan di mana pool itu hidup.
Kesalahan pertama saya: menganggap pool besar selalu lebih baik
refleksi jujur: dulu saya set connection_limit=100 di Prisma karena "biar aman, yang penting tidak habis". Justru itu yang memperparah insiden. Pool yang terlalu besar membuat database mengerjakan terlalu banyak query sekaligus, context switching membengkak, dan tiap query jadi lambat. Query yang lambat membuat koneksi dipinjam lebih lama, yang membuat antrean pool memanjang. Lingkaran setia.
Praktik yang sekarang saya pakai, sejalan dengan saran di komunitas PostgreSQL: pool kecil per instance, bukan pool besar di satu tempat. Rumus kasar yang saya pakai:
pool_size_per_instance = (max_connections × 0.8) / jumlah_instance_aplikasi
Contoh nyata dari sistem yang sekarang saya pegang: max_connections=200, 4 instance aplikasi, sisakan ~40 koneksi untuk opsional (migration, dashboard, psql). Jadi (200 × 0.8) / 4 ≈ 40, saya set 30 per instance supaya ada headroom. Sejak itu puncak koneksi stabil di 120–130, jauh dari batas.
Di Prisma, konfigurasinya lewat connection string:
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=30&pool_timeout=10"
Dua parameter ini yang paling sering diabaikan orang:
connection_limit— jumlah maksimal koneksi yang dibuka Prisma per instance.pool_timeout— berapa lama request menunggu koneksi tersedia sebelum error. Default 10 detik; saya pertahankan, karena lebih baik fail fast daripada request menggantung.

Investigasi: Cara Tahu Koneksi Kamu Bocor atau Cuma Kurang
Sebelum mengubah angka apa pun, saya selalu cek dulu kondisi aktual lewat query ini:
SELECT state, count(*)
FROM pg_stat_activity
WHERE datname = current_database()
GROUP BY state;
Interpretasi hasilnya:
idledominan → kemungkinan koneksi dibuka tapi tidak dipakai/dikembalikan. Cek pool size dan kode yang lupaclient.release().idle in transactionbanyak → ini bahaya. Ada transaksi yang dibuka tapi tidak pernah di-commit/rollback, biasanya bug di kode. Saya pernah bahas pola ini di artikel tentang database transaction dan isolation level dari kasus nyata.activedominan dan CPU database tinggi → query memang berat. Solusinya bukan nambah pool, tapi optimasi query — sering kali berujung ke perbaikan N+1 query di Prisma atau penambahan index PostgreSQL yang tepat.
Poin terakhir ini penting: pool yang lebih besar tidak akan menyelamatkan query yang lambat. Query yang tadinya 2 detik akan tetap 2 detik; kamu cuma menunda kematian.
Kapan Butuh PgBouncer
Kalau arsitektur kamu masih monolik dengan 2–3 instance, pool internal Prisma atau pg.Pool cukup. Tapi begitu jumlah instance naik (auto-scaling, serverless, banyak service share satu database), pool per-instance jadi sulit dikendalikan — inilah saat PgBouncer masuk sebagai pooler eksternal dengan mode transaction.
Dengan mode transaction pooling, satu koneksi backend bisa dipakai bergantian oleh banyak client selama mereka tidak dalam transaksi aktif. Efeknya: 500 client bisa dilayani oleh 20–30 koneksi asli ke PostgreSQL. Trade-offnya nyata: prepared statement bergaya session dan beberapa fitur seperti LISTEN/NOTIFY tidak jalan. Kalau kamu pakai Prisma, set pgbouncer=true di connection string supaya Prisma tidak pakai prepared statement yang bentrok dengan mode transaction.
Untuk best practice spesifik seputar pooling PostgreSQL (termasuk bagaimana menghitung ukuran pool relatif terhadap core dan koneksi aktif), diskusi Stack Overflow tentang connection pooling best practices untuk PostgreSQL adalah referensi yang saya rujuk ulang tiap kali setup proyek baru.
Checklist yang Sekarang Selalu Saya Jalankan
Setelah dua kali kena masalah ini, sekarang saya punya ritual tetap di setiap proyek:
- Hitung angka dulu:
max_connections÷ jumlah instance, sisakan 20% untuk non-aplikasi. - Set
connection_limiteksplisit di connection string, jangan andalkan default. - Pasang alert di
pg_stat_activitysaatcount(*)menyentuh 70% darimax_connections. - Audit query lambat sebelum menambah pool — biasanya biang keroknya di situ.
- Pastikan tidak ada kode yang membuka koneksi manual per request tanpa dikembalikan.
Kalau kamu cuma bisa membawa satu hal dari artikel ini: masalah "kehabisan koneksi" hampir selalu masalah disiplin angka dan query yang lambat, bukan masalah kapasitas database. Naikkan instance database itu langkah terakhir, bukan pertama — dan biasanya tidak diperlukan sama sekali.
Sumber
- Postgres and .Net - Connection Pooling - Best Practices (Stack Overflow) — diskusi best practice menghitung ukuran pool koneksi PostgreSQL relatif terhadap core dan koneksi aktif, jadi acuan saya menentukan
connection_limit.
Tomi Hartanto
Senior Software Engineer
Backend developer dengan 8+ tahun pengalaman membangun sistem scalable. Menulis tentang arsitektur backend, database, dan DevOps.
Artikel Terkait

Soft Delete vs Hard Delete: Saya Kena Masalah di Kedua-Duanya, Ini Cara Saya Memilih Sekarang

Row-Level Security vs Schema-per-Tenant: Saya Coba Keduanya untuk Isolasi Data Multi-Tenant, Ini Hasilnya

Normalisasi vs Denormalisasi: Trade-off Desain Tabel yang Saya Pelajari dari Aplikasi Production
Tomi Hartanto
Senior Software EngineerBackend developer dengan 8+ tahun pengalaman membangun sistem scalable. Menulis tentang arsitektur backend, database, dan DevOps.