Connection Pooling — Database

Connection pool adalah kumpulan koneksi database yang sudah dibuka dan di-reuse oleh aplikasi — alih-alih membuka koneksi baru setiap request. Ini praktik…

Connection pool adalah sekumpulan koneksi database yang dibuka sekali, lalu dipinjamkan bergantian ke setiap request: pinjam, jalankan query, kembalikan. Bayangin menelepon call center bank. Sebelum boleh bertanya, kamu dengar menu, sebut nomor rekening, lalu jawab pertanyaan verifikasi. Kalau pertanyaanmu cuma "saldo saya berapa?", verifikasinya jauh lebih lama dari pertanyaannya. Call center juga cuma punya sekian operator, jadi kalau semuanya sibuk, kamu disuruh menunggu. Membuka koneksi ke database itu verifikasinya, dan jumlah operator itu batas koneksi database.

🧪 Tebakan cepat: pool Kedai Senja berisi 2 koneksi, dan dua-duanya sedang dipakai. Request ketiga datang. Apa yang terjadi: request itu error, pool membuka koneksi ketiga, atau request itu menunggu? Tebak dulu, jawabannya ada di bagian "Pool penuh: antre".

Apa yang terjadi saat aplikasi konek ke database

Satu koneksi (connection) adalah jalur komunikasi antara aplikasi dan server database. Sebelum query pertama bisa dikirim, keduanya harus kenalan dulu: TCP membuka jalur jaringan, TLS menyepakati enkripsi, lalu database mengecek username dan password (autentikasi). PostgreSQL juga menyiapkan satu proses khusus untuk setiap koneksi, yang memakai memory beberapa MB selama koneksinya terbuka.

Semua langkah kenalan ini kita sebut handshake. Di Lab, handshake makan 50 ms dan query-nya 20 ms. Ini angka model contoh, bukan hasil benchmark: database di mesin yang sama bisa tersambung dalam 1 sampai 2 ms, database di region lain bisa ratusan ms. Polanya tetap sama:

Cara Waktu per request (model Lab)
Buka koneksi baru tiap request 70 ms: 50 handshake + 20 query
Pinjam dari pool 20 ms: query saja, handshake sudah dibayar saat koneksinya dibuka

Itu sebabnya aplikasi production butuh pool: membuka koneksi baru (TCP, TLS, autentikasi) untuk setiap request itu boros. Pool memakai ulang koneksi yang sudah tersambung.

Connection pool: pinjam, pakai, kembalikan

Di Node.js, library pg sudah punya pool:

import { Pool } from 'pg'

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 2,                        // paling banyak 2 koneksi
  idleTimeoutMillis: 10000,      // koneksi nganggur 10 detik ditutup
  connectionTimeoutMillis: 2000, // antre lebih dari 2 detik: error
})

app.get('/orders', async (req, res) => {
  // pinjam koneksi, kirim query, kembalikan ke pool: semuanya otomatis
  const { rows } = await pool.query('SELECT * FROM orders ORDER BY id DESC LIMIT 10')
  res.json(rows)
})

Pool penuh: antre

Tiga request datang hampir bersamaan, selisih 2 ms, ke pool berisi 2 koneksi. Ini mode Pool di Lab dengan 3 request:

Waktu Kejadian Aktif Antre
0 ms R1 pinjam koneksi #1 1 0
2 ms R2 pinjam koneksi #2 2 0
4 ms R3 datang, pool penuh: R3 antre 2 1
20 ms R1 selesai, koneksi #1 kembali, R3 langsung memakainya 2 0
22 ms R2 selesai, koneksi #2 kembali 1 0
40 ms R3 selesai 0 0

R3 gak error, dan pool gak membuka koneksi ketiga: R3 menunggu 16 ms sampai ada koneksi yang dikembalikan. Itu jawaban tebakannya.

Latihannya memodelkan persis tabel ini dengan class ConnectionPool(maxSize):

max_connections: batas di sisi database

Database juga punya batas. PostgreSQL menerima paling banyak max_connections koneksi sekaligus, bawaannya 100. Koneksi berikutnya langsung ditolak dengan FATAL: sorry, too many clients already.

Lab memakai model kecil dengan max_connections = 8 supaya batasnya cepat kena. Kirim 12 request tanpa pool: R1 sampai R8 masing-masing membuka koneksi, R9 sampai R12 ditolak dengan pesan di atas. Dengan pool berisi 4 koneksi, ke-12 request selesai dalam 66 ms dan database cuma pernah melihat 4 koneksi. Request yang gak kebagian koneksi menunggu sebentar, bukan ditolak.

Pool hidup di dalam satu proses aplikasi. Kalau aplikasimu jalan di beberapa server (instance), setiap instance punya pool sendiri, dan totalnya dikali: 10 instance dengan max: 20 berarti sampai 200 koneksi, padahal batasnya 100. Aturan praktisnya: max per instance sekitar max_connections dibagi jumlah instance, dikurangi sedikit cadangan. 10 instance dengan batas 100 berarti max sekitar 10, bukan 100.

Laravel dan PHP-FPM

Laravel biasanya jalan di PHP-FPM: sekumpulan proses PHP (worker) yang jumlahnya diatur pm.max_children, dan setiap worker melayani satu request sampai selesai. Laravel membuka koneksi database saat query pertama di sebuah request, lalu koneksinya ditutup saat request selesai. Jadi Laravel di PHP-FPM gak punya pool: setiap request membayar handshake, dan jumlah koneksi sama dengan jumlah worker yang sedang sibuk. 3 server dengan pm.max_children = 50 bisa membuka 150 koneksi.

// config/database.php
'pgsql' => [
    // ...
    'options' => [
        PDO::ATTR_PERSISTENT => true,
    ],
],

Serverless: koneksi berlipat

Di serverless seperti AWS Lambda, setiap instance function melayani satu request pada satu waktu. 300 request bersamaan berarti 300 instance, dan setiap instance punya pool sendiri yang cuma pernah berisi 1 koneksi. Database tetap melihat 300 koneksi. Lebih parah lagi, instance yang sudah selesai tetap hidup sebentar (hangat) menunggu request berikutnya, dan koneksinya ikut tetap terbuka walau gak dipakai.

Solusinya pooler eksternal: server kecil di depan database yang memegang sedikit koneksi ke database dan meminjamkannya ke banyak client.

300 function ──┐
               ├──▶ PgBouncer :6432 ──▶ PostgreSQL :5432
Laravel, Node ─┘    300 client          20 koneksi

📌 Ukuran pool, mode PgBouncer, dan memantau koneksi

⚠️ Jebakan yang sering ketemu

const client = await pool.connect()
try {
  await client.query('BEGIN')
  await client.query('UPDATE orders SET total = 40000 WHERE id = 20')
  await client.query('COMMIT')
} catch (err) {
  await client.query('ROLLBACK')
  throw err
} finally {
  client.release() // jalan saat sukses maupun gagal
}

TL;DR: Membuka koneksi database butuh handshake (TCP, TLS, autentikasi), yang di Lab makan 50 ms sementara query-nya cuma 20 ms. Connection pool membuka koneksi sekali dan meminjamkannya bergantian: pinjam, query, kembalikan. Saat semua koneksi dipakai, request berikutnya antre sampai ada yang di-release, bukan error. Database punya max_connections (bawaan PostgreSQL 100); lewat dari itu, koneksi ditolak dengan FATAL: sorry, too many clients already. Pool hidup per proses, jadi total koneksi = jumlah instance × max. Laravel di PHP-FPM gak punya pool, dan serverless membuat satu koneksi per instance, jadi untuk banyak server atau serverless pasang PgBouncer atau RDS Proxy. Selalu client.release() di finally.

Yang akan kamu pelajari