Connection Pooling PostgreSQL: Pool, Ukuran, dan Mode yang Tepat
Cara mengelola koneksi PostgreSQL, transaksi, pool size, Supavisor session dan transaction mode, timeout, serta autoscaling.
0xNN · · 10 min read
Connection Pooling PostgreSQL: Memilih Pool, Ukuran, dan Mode yang Tepat
PostgreSQL memakai proses backend untuk setiap koneksi. Membuka koneksi baru pada setiap request menambah handshake, autentikasi, TLS, dan konsumsi resource. Solusinya bukan sekadar menaikkan max_connections, melainkan membatasi dan menggunakan ulang koneksi secara sadar.
Ada dua lapisan yang sering disebut “pool”:
1. application-side pool, misalnya pg.Pool di Node.js;
2. external pooler, misalnya Supavisor atau PgBouncer di antara aplikasi dan PostgreSQL.
Keduanya dapat dipakai bersama, tetapi jumlah pool aplikasi di seluruh instance harus tetap diperhitungkan.
---
Satu Pool per Proses Aplikasi
Contoh dasar dengan node-postgres:
import pg from 'pg';
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
connectionTimeoutMillis: 3_000,
idleTimeoutMillis: 30_000,
maxLifetimeSeconds: 300,
});
export async function listUsers() {
const result = await pool.query(
'select id, email from users order by id desc limit $1',
[20],
);
return result.rows;
}
pool.query() meminjam lalu mengembalikan koneksi secara otomatis untuk satu query. Jangan membuat new Pool() di setiap request.
Jika aplikasi berjalan pada 8 process/container dengan max: 10, potensi koneksi aplikasi bukan 10, melainkan 80. Sisakan kapasitas untuk migrasi, console admin, worker, autentikasi, dan layanan database lain.
---
Transaksi Harus Menggunakan Client yang Sama
Transaksi tidak boleh disebar melalui beberapa panggilan pool.query() karena masing-masing dapat memakai koneksi berbeda:
export async function transferBalance(fromId, toId, amount) {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query(
'update wallets set balance = balance - $1 where id = $2',
[amount, fromId],
);
await client.query(
'update wallets set balance = balance + $1 where id = $2',
[amount, toId],
);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
finally wajib agar error tidak membocorkan client dari pool. Pada sistem nyata, transfer juga memerlukan validasi saldo, locking atau isolation yang sesuai, idempotency, serta pencatatan ledger; contoh di atas hanya menunjukkan lifecycle koneksi.
---
Direct, Session Pooling, dan Transaction Pooling
Direct connection menghubungkan client langsung ke PostgreSQL. Cocok untuk migrasi, pg_dump, replikasi, atau backend persisten jika jaringan mendukung dan jumlah koneksi terkendali.
Session pooling memberikan satu koneksi PostgreSQL kepada client selama sesi client. State level-session seperti temporary table, sebagian prepared statement, atau LISTEN lebih mudah dipertahankan, tetapi efisiensi multiplexing lebih rendah.
Transaction pooling hanya meminjamkan koneksi backend selama transaksi. Banyak client dapat berbagi lebih sedikit koneksi database, cocok untuk serverless/edge yang membuat banyak koneksi sementara. Konsekuensinya, aplikasi tidak boleh mengandalkan state yang tetap ada di koneksi berikutnya.
Pada Supabase saat ini:
• direct endpoint umumnya memakai port 5432 dan IPv6, kecuali proyek memiliki IPv4 add-on;
• Supavisor session mode memakai port 5432 pada host pooler;
• Supavisor transaction mode memakai port 6543 pada host pooler;
• frontend/browser semestinya memakai Data API, bukan connection string PostgreSQL.
Ambil connection string dari tombol Connect di dashboard proyek, jangan menyusun host sendiri.
---
Prepared Statement: Jangan Menggeneralisasi
Parameter query tidak otomatis berarti SQL PREPARE. Pada node-postgres, query tanpa properti name adalah query parameterized biasa; pemberian name membuat prepared statement dicache per connection:
await pool.query({
name: 'user-by-id',
text: 'select id, email from users where id = $1',
values: [userId],
});
Supabase menyatakan transaction mode Supavisor tidak mendukung prepared statements. Karena state session dapat berpindah, hindari prepared statements bernama atau ikuti konfigurasi driver yang disarankan dokumentasi Supabase.
Jangan mengganti parameter dengan interpolasi string. Parameterized query tetap diperlukan untuk mencegah SQL injection:
// Benar
await pool.query('select id from users where email = $1', [email]);
// Salah: input pengguna masuk langsung ke SQL
await pool.query(select id from users where email = '${email}');
---
Menentukan Ukuran Pool
Tidak ada angka universal. Pool terlalu kecil menyebabkan antrean; terlalu besar membuat database berebut CPU, memory, dan I/O.
Mulai dari batas yang konservatif, lalu ukur:
• jumlah instance maksimum saat autoscaling;
• koneksi aktif dan idle pada PostgreSQL;
• pool.totalCount, pool.idleCount, dan pool.waitingCount;
• waktu tunggu memperoleh koneksi;
• durasi query dan transaksi;
• headroom terhadap batas database dan layanan lain.
Contoh metrik lokal:
setInterval(() => {
metrics.gauge('db_pool_total', pool.totalCount);
metrics.gauge('db_pool_idle', pool.idleCount);
metrics.gauge('db_pool_waiting', pool.waitingCount);
}, 10_000).unref();
Antrean yang tumbuh tidak selalu berarti connection leak. Penyebab lain termasuk query lambat, transaksi panjang, lock, database overload, atau lonjakan traffic. Korelasikan metrik pool dengan pg_stat_activity, slow query log, dan observability aplikasi.
---
Timeout dan Shutdown
Tanpa timeout, request dapat menunggu koneksi terlalu lama dan menumpuk:
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
connectionTimeoutMillis: 3_000,
idleTimeoutMillis: 30_000,
});
Saat process berhenti, hentikan menerima request baru, tunggu pekerjaan aktif dengan batas waktu, lalu tutup pool:
process.on('SIGTERM', async () => {
server.close(async () => {
await pool.end();
process.exit(0);
});
});
Implementasi production perlu batas waktu paksa agar shutdown tidak menggantung selamanya.
---
Waspadai Autoscaling dan Serverless
Pada platform serverless, satu lonjakan dapat membuat banyak instance sekaligus. Jika setiap instance membuka pool berisi 10 koneksi, batas database bisa habis sebelum metrics autoscaling terlihat. Transaction pooler membantu multiplexing, tetapi aplikasi tetap perlu membatasi concurrency dan menggunakan timeout.
Hindari membuat pool baru di dalam handler. Simpan pool pada scope module agar dapat digunakan ulang ketika runtime mempertahankan instance. Namun jangan menganggap instance akan hidup selamanya: kode harus aman ketika runtime membekukan atau menghentikannya.
Untuk job queue, batas worker sering lebih berguna daripada pool besar. Jika hanya lima job boleh menulis secara paralel, pool 50 tidak membuat pekerjaan selesai sepuluh kali lebih cepat; ia mungkin hanya meningkatkan lock contention dan tekanan I/O.
---
Checklist Operasional
1. Buat satu pool yang dipakai ulang per proses.
2. Hitung pool max × instance maksimum, bukan hanya ukuran satu pool.
3. Gunakan client yang sama dan finally untuk transaksi.
4. Pilih direct/session/transaction mode sesuai lifecycle aplikasi.
5. Pastikan fitur driver kompatibel dengan mode pooler.
6. Pasang timeout, metrik antrean, dan graceful shutdown.
7. Ubah ukuran berdasarkan beban terukur, bukan rumus tunggal.
Pooling melindungi database dari ledakan koneksi, tetapi tidak memperbaiki query lambat atau transaksi yang terlalu panjang. Ia bekerja paling baik bersama batas concurrency dan observability.
Referensi
• Supabase: Connect to your database
• Supabase: Connection management
• Supabase: Supavisor and connection terminology
• node-postgres: Pooling
• node-postgres: Queries