Database Connection Pool: Konsep, Masalah Umum, dan Best Practice (Contoh Golang + GORM)
9 min read

Database Connection Pool: Konsep, Masalah Umum, dan Best Practice (Contoh Golang + GORM)

Dalam aplikasi backend modern, database connection pool adalah salah satu komponen paling krusial namun sering diremehkan. Banyak masalah performa, latency tinggi, bahkan outage di production bukan disebabkan oleh query yang lambat, melainkan oleh connection pool yang salah konfigurasi. Artikel ini membahas konsep connection pool secara realistis — bukan sekadar definisi textbook — beserta masalah umum yang muncul kalau tidak disetel dengan benar, cara kerjanya di Go, contoh implementasi dengan GORM, dan cara berpikir yang tepat untuk menentukan angka konfigurasi yang ideal.

Apa Itu Database Connection Pool?

Secara sederhana, connection pool adalah sekumpulan koneksi database yang dipertahankan dan digunakan ulang oleh aplikasi, bukan dibuka dan ditutup setiap kali ada request baru. Tanpa pool, setiap request akan membuka koneksi baru ke database, menjalankan query, lalu menutup koneksi tersebut setelah selesai — proses ini membawa overhead besar karena setiap koneksi baru membutuhkan TCP handshake, negosiasi TLS, dan proses autentikasi dari awal.

Dengan pool, koneksi dibuat di awal, disimpan di dalam pool, lalu dipakai ulang oleh banyak request secara bergantian. Request yang membutuhkan koneksi akan “meminjam” satu koneksi dari pool, memakainya untuk menjalankan query, lalu mengembalikannya ke pool setelah selesai — bukan menutup koneksi tersebut secara permanen.

sequenceDiagram
    participant App as Aplikasi
    participant Pool as Connection Pool
    participant DB as Database

    App->>Pool: Minta koneksi
    alt Koneksi tersedia di pool
        Pool-->>App: Berikan koneksi yang sudah ada
    else Pool kosong, belum mencapai MaxOpenConns
        Pool->>DB: Buka koneksi baru
        DB-->>Pool: Koneksi siap
        Pool-->>App: Berikan koneksi baru
    end
    App->>DB: Jalankan query
    DB-->>App: Hasil query
    App->>Pool: Kembalikan koneksi

Kenyataan di Production

Beberapa fakta berikut sering luput dari perhatian sampai masalah benar-benar terjadi di production:

  • Koneksi database itu mahal — jauh lebih mahal dibanding membuat goroutine baru di Go
  • Database punya limit koneksi maksimum yang bisa ditangani secara bersamaan
  • Terlalu banyak koneksi akan membuat database overload
  • Terlalu sedikit koneksi akan membuat request saling antre, sehingga latency naik

Connection pool pada dasarnya adalah alat untuk mengontrol tekanan yang diberikan aplikasi ke database — bukan sekadar fitur optimasi performa yang opsional.


Masalah Umum Tanpa atau Salah Konfigurasi Connection Pool

Default Setting adalah yang Paling Berbahaya

Banyak engineer berasumsi bahwa karena sudah memakai GORM atau database/sql, konfigurasi pool otomatis aman. Faktanya, default dari database/sql di Go justru berpotensi berbahaya kalau tidak disesuaikan: MaxOpenConns defaultnya 0 yang berarti unlimited, MaxIdleConns defaultnya hanya 2, dan tidak ada timeout default yang jelas untuk lifetime koneksi.

Kombinasi ini berbahaya karena saat traffic spike terjadi, aplikasi bisa membuka ribuan koneksi tanpa batas sampai database kehabisan resource. Dari sisi aplikasi, gejalanya sering terlihat sebagai “random timeout” yang membingungkan, padahal akar masalahnya adalah jumlah koneksi yang tidak terkendali.

Max Connection Terlalu Besar

Skenario umum yang sering terjadi: database punya limit 100 koneksi, sementara aplikasi berjalan di 5 pod, dan masing-masing pod diset MaxOpenConns = 50. Secara matematis, total koneksi maksimum yang mungkin terjadi adalah:

5 pod × 50 MaxOpenConns = 250 koneksi

Angka ini jauh melebihi limit 100 koneksi yang disediakan database. Begitu traffic cukup tinggi sehingga semua pod mencoba memakai koneksi mendekati batas masing-masing, database akan langsung kehabisan slot koneksi dan menolak koneksi baru — bukan karena query yang berat, tapi karena jumlah koneksi yang melebihi kapasitas.

Max Connection Terlalu Kecil

Kebalikannya juga bermasalah. Kalau MaxOpenConns diset terlalu kecil dibanding kebutuhan aktual, banyak goroutine akan menunggu koneksi yang tersedia. Akibatnya, CPU terlihat idle (karena goroutine yang menunggu tidak memakai CPU secara aktif), tapi latency tetap tinggi karena request harus mengantre sebelum benar-benar bisa mengeksekusi query.

Masalah ini sering terjadi khususnya di Go karena goroutine sangat murah untuk dibuat — ribuan goroutine bisa berjalan bersamaan tanpa masalah berarti — sementara koneksi database tetap mahal dan terbatas. Kemudahan membuat goroutine ini sering membuat developer lupa bahwa resource di sisi database tidak ikut scaling secara otomatis mengikuti jumlah goroutine yang dibuat.

Jangan langsung menyalahkan query yang lambat kalau melihat latency tinggi dengan CPU yang rendah. Kombinasi ini justru sering jadi indikasi bottleneck di connection pool — goroutine menunggu koneksi yang tersedia, bukan menunggu hasil query yang berat.

Cara Kerja Connection Pool di Go

Di Go, connection pool sebenarnya dikelola oleh package database/sql, bukan oleh ORM seperti GORM. GORM hanya membungkus database/sql untuk memberikan API yang lebih nyaman dipakai — semua pengaturan pool tetap dilakukan di level sql.DB, objek yang merepresentasikan pool koneksi itu sendiri.

flowchart LR
    A[Kode aplikasi] --> B[GORM]
    B --> C[database/sql]
    C --> D[Koneksi database aktual]

Implikasi praktisnya penting untuk dipahami: kalau kamu memakai GORM, kamu tetap wajib mengatur connection pool secara eksplisit. GORM tidak otomatis memberikan konfigurasi pool yang aman hanya karena ia berjalan di atas database/sql — pengaturan default dari database/sql tetap berlaku sampai diubah secara manual.


Contoh Implementasi Golang + GORM

Setup Database

Inisialisasi koneksi database dengan GORM dimulai seperti biasa:

db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
    log.Fatal(err)
}

Karena seluruh konfigurasi pool dilakukan di level sql.DB, langkah berikutnya adalah mengambil objek tersebut dari instance GORM:

sqlDB, err := db.DB()
if err != nil {
    log.Fatal(err)
}

Method db.DB() mengembalikan pointer ke *sql.DB yang dipakai GORM di balik layar. Objek inilah yang dipakai untuk seluruh konfigurasi pool di bagian berikutnya.


Konfigurasi Connection Pool (Wajib Dilakukan)

Max Open Connections

sqlDB.SetMaxOpenConns(20)

Konfigurasi ini menentukan maksimal 20 koneksi aktif yang boleh dibuka ke database secara bersamaan. Kalau seluruh 20 koneksi sedang dipakai dan ada request baru yang membutuhkan koneksi, request tersebut akan menunggu sampai salah satu koneksi yang sedang dipakai dikembalikan ke pool.

Best practice untuk nilai ini: jangan memakai angka besar tanpa alasan yang jelas. Hitung berdasarkan dua faktor utama — limit maksimum koneksi yang diizinkan database, dan jumlah instance aplikasi yang berjalan bersamaan, karena setiap instance akan memiliki pool-nya sendiri yang terpisah.

Max Idle Connections

sqlDB.SetMaxIdleConns(10)

Konfigurasi ini menentukan berapa banyak koneksi yang menganggur (tidak sedang dipakai) tetap disimpan di pool, bukan langsung ditutup. Menyimpan koneksi idle menghindari overhead membuka koneksi baru dari awal setiap kali ada request, karena koneksi yang sudah ada bisa langsung dipakai ulang.

Best practice: biasanya diset sekitar 50–75% dari MaxOpenConns, dan jangan pernah lebih besar dari MaxOpenConns itu sendiri — secara logis tidak ada gunanya menyimpan lebih banyak koneksi idle dibanding total koneksi maksimum yang diizinkan.

Connection Max Lifetime

sqlDB.SetConnMaxLifetime(30 * time.Minute)

Konfigurasi ini memaksa setiap koneksi untuk ditutup setelah jangka waktu tertentu, terlepas dari apakah koneksi tersebut sedang dipakai secara aktif atau tidak. Tujuannya adalah mencegah masalah yang muncul dari koneksi “tua” — koneksi yang sudah terbuka terlalu lama berisiko mengalami masalah network yang tidak terdeteksi, atau terputus paksa oleh load balancer maupun proxy database yang punya kebijakan timeout sendiri.

Best practice: nilai yang umum dipakai berkisar 15–60 menit, dan harus lebih kecil dibanding idle timeout yang diterapkan di sisi database atau proxy database (seperti PgBouncer). Kalau nilai ini lebih besar dari timeout di sisi database, koneksi bisa saja sudah ditutup paksa oleh database sementara aplikasi masih menganggapnya valid.

Connection Max Idle Time (Go ≥ 1.15)

sqlDB.SetConnMaxIdleTime(10 * time.Minute)

Konfigurasi ini menutup koneksi yang sudah idle (tidak dipakai) terlalu lama, meskipun belum mencapai ConnMaxLifetime. Tujuannya adalah menghemat resource — koneksi yang jarang dipakai tidak perlu terus dipertahankan terbuka kalau memang tidak ada traffic yang membutuhkannya.

Best practice: nilai 5–15 menit umumnya cukup, dan opsi ini sangat cocok untuk traffic yang tidak stabil — periode sepi diikuti lonjakan traffic — karena koneksi yang tidak terpakai saat periode sepi akan otomatis ditutup, mengurangi beban idle ke database.


Contoh Konfigurasi Ideal (Umum)

Untuk satu service instance dengan traffic menengah, berikut kombinasi konfigurasi yang sering dipakai sebagai titik awal:

sqlDB.SetMaxOpenConns(20)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(30 * time.Minute)
sqlDB.SetConnMaxIdleTime(10 * time.Minute)

Kombinasi ini bukan angka ajaib yang berlaku universal — ia hanya titik awal yang wajar untuk kemudian disesuaikan berdasarkan karakteristik traffic dan limit database yang sebenarnya, sesuai cara berpikir yang dijelaskan di bagian berikutnya.


Cara Menentukan Angka yang Benar

Lihat Limit Database

Langkah pertama adalah memeriksa limit koneksi maksimum yang diizinkan database. Misalnya untuk PostgreSQL:

Postgres max_connections = 100

Nilai ini bisa dicek lewat konfigurasi postgresql.conf atau dengan query SHOW max_connections; langsung ke database.

Hitung Jumlah Instance

Selanjutnya, hitung berapa banyak instance aplikasi yang akan terhubung ke database tersebut secara bersamaan, misalnya:

5 pod aplikasi

Bagi Secara Aman

Bagi limit database dengan jumlah instance untuk mendapatkan estimasi awal MaxOpenConns per instance:

100 / 5 = 20

Sehingga MaxOpenConns = 20 jadi titik awal yang wajar. Tapi jangan langsung memakai seluruh kapasitas — sisakan buffer untuk kebutuhan lain yang juga memakai koneksi ke database yang sama, seperti proses migration, akses admin manual, atau background job yang berjalan terpisah dari traffic utama aplikasi.

Sesuaikan dengan Karakteristik Traffic

Angka hasil pembagian di atas masih perlu disesuaikan lagi berdasarkan jenis query yang dijalankan. Kalau mayoritas query cepat (di bawah puluhan milidetik), MaxOpenConns yang lebih kecil biasanya tetap cukup karena koneksi cepat dikembalikan ke pool. Sebaliknya, kalau ada query berat atau lama (laporan kompleks, agregasi besar), jangan menetapkan MaxOpenConns terlalu besar untuk jenis query ini, karena setiap koneksi akan tertahan lebih lama, sehingga lebih cepat mendekati limit yang ditetapkan.


Anti-Pattern yang Harus Dihindari

✗ Tidak mengatur connection pool sama sekali, mengandalkan default database/sql
✗ Mengatur MaxOpenConns dengan angka besar "biar aman" tanpa perhitungan
✗ Menganggap database bisa autoscale seperti aplikasi stateless
✗ Memakai satu database untuk terlalu banyak service tanpa perhitungan kapasitas total

Keempat anti-pattern ini punya kesamaan: semuanya berasal dari asumsi bahwa database punya elastisitas yang sama dengan komponen aplikasi yang stateless. Aplikasi bisa di-scale dengan menambah pod atau instance baru hampir tanpa batas, tapi database tidak bekerja seperti itu — kapasitas koneksinya tetap terbatas pada satu titik (atau cluster) tertentu, dan setiap instance aplikasi baru yang ditambahkan justru menambah tekanan ke kapasitas yang sama, bukan membaginya secara otomatis.


Observability: Jangan Berjalan Buta

Konfigurasi yang benar di awal tidak cukup tanpa monitoring berkelanjutan, karena karakteristik traffic bisa berubah seiring waktu. Beberapa metrik yang wajib dipantau:

  • Jumlah active connections (koneksi yang sedang dipakai aktif)
  • Jumlah idle connections (koneksi yang menganggur di pool)
  • Wait time — berapa lama request menunggu sebelum mendapat koneksi dari pool

Sebagian besar database driver Go menyediakan metrik ini lewat method sqlDB.Stats(), yang mengembalikan struct berisi informasi seperti OpenConnections, InUse, Idle, dan WaitDuration — metrik-metrik ini bisa diekspos ke sistem monitoring seperti Prometheus untuk dipantau secara berkelanjutan.

Pola yang perlu diwaspadai: kalau CPU rendah, latency tinggi, dan banyak goroutine dalam keadaan blocked menunggu sesuatu, kemungkinan besar bottleneck-nya ada di connection pool — bukan di query atau resource compute aplikasi. Gejala ini sering disalahartikan sebagai masalah performa kode, padahal solusinya ada di konfigurasi pool, bukan di optimasi query.


Ringkasan

  • Connection pool adalah kumpulan koneksi database yang dipakai ulang, menghindari overhead membuka koneksi baru di setiap request.
  • Default database/sql di Go (MaxOpenConns unlimited, MaxIdleConns = 2) berisiko di production dan harus selalu diatur ulang secara eksplisit.
  • GORM tidak mengatur connection pool secara otomatis — konfigurasi tetap dilakukan lewat *sql.DB yang diambil dari db.DB().
  • Hitung MaxOpenConns berdasarkan limit koneksi database dibagi jumlah instance aplikasi, sisakan buffer untuk migration dan background job.
  • MaxIdleConns idealnya 50–75% dari MaxOpenConns; ConnMaxLifetime dan ConnMaxIdleTime mencegah koneksi “tua” dan menghemat resource saat traffic sepi.
  • Hindari anti-pattern seperti tidak mengatur pool sama sekali, atau mengatur MaxOpenConns terlalu besar “biar aman” tanpa perhitungan.
  • Monitor active connections, idle connections, dan wait time — CPU rendah dengan latency tinggi sering jadi tanda bottleneck di connection pool, bukan di query.
  • Rule of thumb: lebih baik request menunggu koneksi sebentar daripada database mati karena kelebihan koneksi.

Portofolio