Langsung ke konten
Connection Pooling: Kenapa Aplikasi Backend Kamu Sering Kehabisan Koneksi Database
Kembali ke Blog
Database·6 menit baca·

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.

TH
Tomi Hartanto·Senior Software Engineer

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.

Ilustrasi banyak koneksi database menumpuk menabrak batas max connections PostgreSQL

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.

Ilustrasi connection pool dengan koneksi dipinjam dan dikembalikan oleh request

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:

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:

  1. Hitung angka dulu: max_connections ÷ jumlah instance, sisakan 20% untuk non-aplikasi.
  2. Set connection_limit eksplisit di connection string, jangan andalkan default.
  3. Pasang alert di pg_stat_activity saat count(*) menyentuh 70% dari max_connections.
  4. Audit query lambat sebelum menambah pool — biasanya biang keroknya di situ.
  5. 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

TH

Tomi Hartanto

Senior Software Engineer

Backend developer dengan 8+ tahun pengalaman membangun sistem scalable. Menulis tentang arsitektur backend, database, dan DevOps.