Connection pool habis saat traffic naik di aplikasi Spring Boot biasanya bukan sekadar masalah “pool terlalu kecil”. Gejalanya terlihat jelas: latency melonjak, request mulai timeout, thread aplikasi menumpuk, dan log HikariCP menampilkan kegagalan mendapatkan koneksi dalam batas waktu. Dalam banyak kasus, akar masalahnya justru ada di pola transaksi, query yang lambat, atau koneksi yang tertahan terlalu lama.

Pada artikel ini kita bahas studi kasus debugging backend di Spring Boot dengan pendekatan yang realistis: mulai dari gejala, kronologi investigasi, hipotesis awal yang ternyata salah, sampai root cause dan langkah perbaikan. Tujuannya bukan hanya memulihkan sistem, tetapi memahami mengapa connection pool habis saat traffic naik dan bagaimana mencegahnya terulang.

Konteks arsitektur singkat

Kasus ini terjadi pada layanan backend Spring Boot yang melayani API HTTP untuk aplikasi web dan mobile. Layanan ini menggunakan:

  • Spring Boot untuk REST API
  • JPA/Hibernate untuk akses database relasional
  • HikariCP sebagai connection pool
  • Spring Actuator untuk metrics dan health endpoint
  • Pemanggilan HTTP ke service eksternal di beberapa alur bisnis

Pada kondisi normal, latency stabil dan penggunaan connection pool relatif rendah. Masalah mulai muncul ketika traffic meningkat, misalnya karena kampanye promosi, job terjadwal, atau lonjakan request paralel dari klien.

Gejala nyata saat insiden terjadi

Gejala di level aplikasi biasanya muncul berurutan, bukan langsung crash total:

  • Latency API naik tajam, terutama endpoint yang menyentuh database.
  • Request timeout dari gateway, load balancer, atau client.
  • Thread menumpuk karena banyak request menunggu resource yang sama.
  • Error HikariCP seperti timeout saat mengambil koneksi dari pool.
  • Throughput turun walau CPU belum tentu 100%.

Contoh log yang umum terlihat:

HikariPool-1 - Connection is not available, request timed out after 30000ms.

Jika memakai Spring Data JPA atau JDBC, error lanjutan bisa muncul sebagai exception turunan data access, misalnya kegagalan mendapatkan koneksi untuk menjalankan query atau membuka transaksi.

Catatan penting: connection pool habis bukan selalu berarti database down. Sering kali database masih hidup, tetapi koneksi aktif tertahan terlalu lama sehingga request baru tidak kebagian koneksi.

Kronologi investigasi: dari dugaan awal sampai root cause

1. Dugaan awal: ukuran pool terlalu kecil

Hipotesis pertama yang paling umum adalah max pool size HikariCP terlalu kecil. Secara intuitif ini masuk akal: traffic naik, koneksi habis, berarti tambahkan jumlah koneksi. Namun pendekatan ini sering hanya menunda masalah.

Kalau akar masalahnya adalah koneksi dipegang terlalu lama, memperbesar pool hanya membuat:

  • lebih banyak query lambat berjalan bersamaan,
  • beban database makin tinggi,
  • waktu pemulihan makin lama saat bottleneck terjadi.

Karena itu, sebelum mengubah ukuran pool, kita perlu melihat apakah koneksi memang kurang atau justru tidak cepat dilepas.

2. Validasi metrik pool dan request

Dari Spring Actuator dan metrics HikariCP, pola yang biasanya terlihat saat insiden:

  • Jumlah active connections mendekati batas maksimum.
  • Jumlah idle connections turun ke nol atau sangat rendah.
  • Jumlah thread yang pending untuk menunggu koneksi meningkat.
  • Latency endpoint yang berhubungan dengan database ikut naik.

Jika tersedia endpoint metrics, periksa metrik yang berkaitan dengan Hikari dan server thread. Nama metrik dapat berbeda bergantung integrasi monitoring, tetapi idenya sama: lihat apakah pool jenuh dan apakah antrean permintaan koneksi bertambah.

Contoh pemeriksaan sederhana:

curl http://localhost:8080/actuator/metrics
curl http://localhost:8080/actuator/metrics/hikaricp.connections.active
curl http://localhost:8080/actuator/metrics/hikaricp.connections.idle
curl http://localhost:8080/actuator/metrics/hikaricp.connections.pending

Jika metrik menunjukkan koneksi aktif penuh dalam durasi lama, masalahnya bukan spike sesaat. Ada indikasi kuat koneksi tertahan di level aplikasi atau query yang berjalan terlalu lama.

3. Thread dump: apakah request menunggu koneksi?

Langkah berikutnya adalah mengambil thread dump saat insiden aktif. Tujuannya untuk melihat apakah banyak thread request berhenti di area yang berkaitan dengan data source, transaksi, atau query execution.

Perintah umum di lingkungan JVM:

jstack <pid> > thread-dump.txt

Dari thread dump, cari pola seperti:

  • thread HTTP worker yang sedang menunggu pengambilan koneksi,
  • thread yang tertahan di JDBC call atau eksekusi query,
  • thread yang sedang menunggu respons service eksternal tetapi masih berada dalam transaksi.

Ini penting karena bottleneck connection pool sering terlihat sebagai gejala di thread aplikasi: thread request tidak selesai, sehingga koneksi tidak kembali ke pool.

4. Analisis log transaksi dan query

Setelah itu, korelasikan waktu lonjakan latency dengan log aplikasi:

  • apakah ada endpoint tertentu yang mendominasi traffic,
  • apakah ada query yang durasinya meningkat,
  • apakah ada pemanggilan service eksternal di tengah proses yang juga menyentuh database,
  • apakah ada exception retry atau timeout berulang.

Di tahap ini, tim sering menemukan bahwa masalah hanya muncul pada alur tertentu, misalnya endpoint checkout, sinkronisasi data, atau proses bulk update.

Root cause yang ditemukan

Pada studi kasus seperti ini, root cause umumnya bukan tunggal. Ada satu penyebab utama, lalu diperburuk oleh faktor lain. Kombinasi yang paling sering adalah:

Transaksi terlalu panjang

Service method diberi @Transactional, lalu di dalamnya ada banyak langkah: baca data, validasi, hit API eksternal, hitung bisnis, simpan perubahan, dan kirim event. Selama transaksi masih aktif, koneksi database bisa tetap terasosiasi dengan proses tersebut lebih lama dari yang diperlukan.

Contoh pola yang bermasalah:

@Service
public class OrderService {

    @Transactional
    public void createOrder(CreateOrderRequest request) {
        Customer customer = customerRepository.findById(request.customerId())
            .orElseThrow();

        Inventory inventory = inventoryRepository.lockByProductId(request.productId());

        // Masalah: call eksternal di dalam transaksi
        PaymentResponse payment = paymentClient.charge(request);

        Order order = new Order(customer, request.productId(), payment.reference());
        orderRepository.save(order);

        inventory.decrease(request.quantity());
    }
}

Masalahnya bukan hanya durasi method yang panjang. Call ke service eksternal dapat melambat, retry, atau timeout. Akibatnya transaksi terbuka lebih lama, koneksi lebih lama tidak kembali ke pool, dan saat traffic naik, pool cepat habis.

Query lambat atau tidak efisien

Setelah analisis query, sering ditemukan satu atau beberapa query dengan durasi tinggi, misalnya karena:

  • kolom filter belum terindeks,
  • query menghasilkan full scan pada tabel besar,
  • join berlebihan,
  • efek N+1 query dari ORM,
  • sort atau pagination yang mahal.

Satu query lambat tidak selalu mematikan sistem saat traffic rendah. Tetapi ketika request paralel meningkat, banyak koneksi akan sibuk lebih lama di database, sehingga pool menjadi bottleneck.

Pemanggilan eksternal di dalam transaksi

Ini sering menjadi akar masalah yang paling tersembunyi. Di atas kertas, transaksi terlihat benar karena semua langkah bisnis “dibungkus” agar konsisten. Dalam praktik, resource database jadi tertahan sambil menunggu jaringan, service pihak ketiga, atau antrean internal yang tidak stabil.

Jika endpoint eksternal melambat beberapa detik, puluhan request paralel bisa menahan puluhan koneksi aktif. Di titik itu, request lain yang sebenarnya hanya perlu query cepat ikut gagal karena tidak mendapat koneksi.

Langkah diagnosis yang efektif

1. Aktifkan logging yang relevan, bukan semua log

Hindari menyalakan log terlalu detail di seluruh aplikasi saat insiden, karena justru bisa menambah beban. Fokus pada:

  • log timeout koneksi HikariCP,
  • log durasi request per endpoint,
  • log query lambat jika tersedia di database atau lapisan observabilitas,
  • log pemanggilan service eksternal beserta durasi dan timeout.

Jika perlu, tambahkan correlation id agar satu request bisa ditelusuri dari controller, service, query, sampai outbound HTTP.

2. Baca metrics dari Actuator secara berkala

Jangan hanya melihat snapshot sekali. Ambil metrik beberapa kali saat insiden berlangsung. Pola yang perlu dicari:

  • active connections terus penuh,
  • pending requests ke pool naik,
  • request latency naik seiring penuhnya pool,
  • error rate meningkat setelah antrean koneksi membesar.

Jika memungkinkan, kirim metrik ini ke sistem monitoring agar mudah dikorelasikan dengan traffic, error rate, dan performa database.

3. Ambil thread dump lebih dari sekali

Satu thread dump hanya memberi foto sesaat. Ambil beberapa dump dengan jarak pendek untuk melihat apakah thread benar-benar stuck di lokasi yang sama. Jika banyak thread selalu menunggu koneksi atau tertahan di query yang sama, itu sinyal kuat penyebab utama.

4. Analisis query di database

Pemeriksaan di sisi database sangat penting. Cari:

  • query yang durasinya paling lama,
  • query yang paling sering dieksekusi,
  • lock atau blocking antar transaksi,
  • rencana eksekusi yang buruk.

Jangan berhenti di aplikasi. Connection pool habis sering merupakan gejala dari masalah query dan transaksi di lapisan bawah.

Perbaikan kode: pendekkan durasi transaksi

Prinsip utamanya adalah jangan tahan transaksi lebih lama dari yang diperlukan. Operasi database sebaiknya dipisahkan dari pemanggilan eksternal bila konsistensi bisnis masih bisa dijaga dengan desain yang tepat.

Contoh refactor sederhana:

@Service
public class OrderService {

    private final PaymentClient paymentClient;
    private final OrderTxService orderTxService;

    public void createOrder(CreateOrderRequest request) {
        // Call eksternal di luar transaksi
        PaymentResponse payment = paymentClient.charge(request);

        // Transaksi hanya untuk pekerjaan database
        orderTxService.persistOrder(request, payment);
    }
}

@Service
class OrderTxService {

    @Transactional
    public void persistOrder(CreateOrderRequest request, PaymentResponse payment) {
        Customer customer = customerRepository.findById(request.customerId())
            .orElseThrow();

        Inventory inventory = inventoryRepository.lockByProductId(request.productId());
        inventory.decrease(request.quantity());

        Order order = new Order(customer, request.productId(), payment.reference());
        orderRepository.save(order);
    }
}

Pendekatan ini bekerja karena koneksi database hanya dipakai saat benar-benar diperlukan. Jika service eksternal lambat, koneksi tidak ikut tertahan di pool.

Tentu ada trade-off. Jika call eksternal sukses tetapi transaksi database gagal, Anda perlu strategi kompensasi, retry, atau desain yang lebih tahan gangguan seperti outbox pattern dan pemrosesan asinkron. Namun untuk banyak sistem, ini tetap lebih aman daripada menahan transaksi sambil menunggu jaringan.

Perbaikan query dan akses data

Optimalkan query lambat

Jika ada query yang dominan lambat, lakukan perbaikan di sumbernya:

  • tambahkan indeks yang sesuai dengan pola filter dan join,
  • hindari mengambil kolom atau relasi yang tidak perlu,
  • batasi hasil dengan pagination yang benar,
  • periksa apakah ORM menghasilkan query yang tidak efisien.

Jika memakai JPA, perhatikan pola lazy loading yang memicu banyak query kecil. Dalam beban tinggi, efek ini dapat menghabiskan waktu koneksi dan memperburuk kepadatan pool.

Batasi kerja di dalam transaksi

Selain call eksternal, hindari hal-hal berikut di dalam transaksi:

  • komputasi berat yang bisa dikerjakan sebelum atau sesudah transaksi,
  • serialisasi payload besar,
  • pembuatan file atau akses storage,
  • retry manual yang memanjang.

Transaksi idealnya hanya mencakup operasi yang benar-benar butuh konsistensi atomik di database.

Konfigurasi Spring Boot dan HikariCP yang relevan

Konfigurasi berikut bukan solusi tunggal, tetapi membantu membuat perilaku pool lebih terukur dan lebih mudah didiagnosis:

spring.datasource.url=jdbc:postgresql://db:5432/app
spring.datasource.username=app_user
spring.datasource.password=secret

spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.validation-timeout=5000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.leak-detection-threshold=20000

management.endpoints.web.exposure.include=health,info,metrics,prometheus

Beberapa catatan penting:

  • maximum-pool-size harus disesuaikan dengan kapasitas database dan pola concurrency aplikasi, bukan dinaikkan tanpa batas.
  • connection-timeout menentukan berapa lama request menunggu koneksi sebelum gagal. Nilai terlalu tinggi bisa membuat antrean makin panjang dan gejala terlambat terlihat.
  • leak-detection-threshold bisa membantu mendeteksi koneksi yang tertahan terlalu lama, tetapi gunakan dengan hati-hati dan jangan dianggap pengganti analisis akar masalah.
  • Pastikan kapasitas thread server, worker async, dan pool database tetap seimbang. Pool besar dengan query lambat tetap akan macet.

Jangan menganggap menaikkan maximum-pool-size sebagai perbaikan utama. Jika database, query, atau transaksi tidak sehat, pool yang lebih besar justru mempercepat saturasi di sisi database.

Checklist debugging saat connection pool habis

  1. Konfirmasi gejala dari log HikariCP dan error aplikasi.
  2. Periksa metrik active, idle, dan pending connections.
  3. Korelasikan lonjakan pool dengan latency endpoint.
  4. Ambil thread dump saat insiden aktif.
  5. Identifikasi endpoint atau service method yang durasinya paling tinggi.
  6. Cari @Transactional yang membungkus call eksternal atau proses panjang.
  7. Analisis query lambat, lock, dan execution plan di database.
  8. Refactor agar transaksi lebih pendek dan query lebih efisien.
  9. Evaluasi ulang ukuran pool setelah akar masalah diperbaiki.

Observabilitas setelah perbaikan

Setelah insiden reda, pekerjaan belum selesai. Anda perlu memastikan perbaikannya benar-benar efektif dan tidak hanya menggeser bottleneck ke tempat lain. Minimum yang sebaiknya dipantau:

  • Request latency per endpoint, idealnya p50, p95, dan p99.
  • Error rate untuk timeout, data access exception, dan upstream failure.
  • HikariCP metrics: active, idle, pending, timeout.
  • Durasi query dan jumlah query per request untuk endpoint kritis.
  • Durasi call eksternal dan jumlah retry.
  • Thread pool saturation di web server atau executor async.

Tambahkan alert yang spesifik, misalnya:

  • active connections mendekati maksimum selama beberapa menit,
  • pending connection request mulai meningkat,
  • timeout mendapatkan koneksi muncul lebih dari ambang tertentu,
  • latency endpoint database-heavy naik bersamaan dengan metrik pool.

Kesalahan umum yang sering memperparah insiden

  • Langsung menaikkan pool size tanpa membuktikan akar masalah.
  • Mengabaikan thread dump karena fokus hanya pada log aplikasi.
  • Tidak memeriksa database padahal query lambat atau lock bisa jadi penyebab utama.
  • Membungkus seluruh alur bisnis dalam satu transaksi demi kemudahan coding.
  • Tidak membedakan bottleneck CPU, thread, dan koneksi, padahal gejalanya bisa mirip.

Penutup

Dalam insiden Debugging Spring Boot: Root Cause Connection Pool Habis saat Traffic Naik, pelajaran terbesarnya adalah: connection pool habis biasanya merupakan gejala dari desain transaksi atau akses data yang tidak sehat di bawah beban. Tanda-tandanya nyata—latency melonjak, request timeout, thread menumpuk, dan HikariCP gagal menyediakan koneksi—tetapi perbaikannya menuntut investigasi yang disiplin.

Mulailah dari metrik, log, dan thread dump. Validasi apakah koneksi memang kurang, atau justru tertahan terlalu lama karena transaksi panjang, query lambat, atau call eksternal di dalam transaksi. Setelah akar masalah ditemukan, perbaiki struktur kode, optimalkan query, dan gunakan konfigurasi HikariCP sebagai penyangga yang tepat, bukan sebagai tambalan utama.