Troubleshooting 28 Aug 2026 11 views 0 komentar

Cara Mengatasi MySQL Too Many Connections - Panduan Lengkap untuk Developer

Cara Mengatasi MySQL Too Many Connections - Panduan Lengkap untuk Developer

Pernah lihat error ini pas aplikasi production tiba-tiba mati?


SQLSTATE[HY000] [1040] Too many connections

Saya masih ingat kejadiannya. Jam sepuluh pagi, aplikasi yang melayani ribuan pengguna tiba-tiba lemot, lalu semua request error. Saya buka log, isinya semua "Too many connections". Panik? Iya, sempat. Tapi setelah beberapa jam debugging, ternyata penyebabnya cukup sederhana dan solusinya jelas.

Artikel ini saya tulis berdasarkan pengalaman itu. Saya bahas kenapa error ini muncul, cara diagnosa cepat, dan solusi yang bisa langsung kamu terapkan - dari yang paling aman sampai yang butuh perubahan konfigurasi. Kalau kamu pakai MySQL atau MariaDB untuk aplikasi web, artikel ini wajib dibaca sampai habis.

Kenapa Error "Too Many Connections" Muncul?

Setiap koneksi ke MySQL itu ada batasnya. Default max_connections di MySQL 8 adalah 151. Artinya, dalam satu waktu, MySQL hanya melayani 151 koneksi. Ketika koneksi ke-152 mencoba masuk, MySQL langsung menolak dengan error 1040 (HY000).

Penting: error ini muncul di sisi aplikasi, bukan di server MySQL. User buka website, lalu PHP atau Node.js mencoba connect ke database, tapi ditolak. Hasilnya, halaman error atau white screen di browser.

Penyebabnya jarang "server kekecilan". Yang paling sering saya temui di lapangan:

  • Connection leak - koneksi database dibuat tapi tidak pernah ditutup, apalagi kalau kode-nya banyak cabang if yang lupa di-close.
  • Traffic spike - jumlah pengguna naik drastis (flash sale, artikel viral, crawler), dan setiap request bikin koneksi baru.
  • Koneksi tidur (Sleep) - koneksi sudah selesai dipakai tapi masih menggantung. Default wait_timeout MySQL 8 jam, jadi koneksi nganggur bisa numpuk.
  • Query lambat - satu query jalan 30 detik, koneksinya dipegang terus, dan request berikutnya bikin koneksi baru lagi.
  • Terlalu banyak aplikasi - beberapa aplikasi (web, cron, API internal) connect ke database yang sama tanpa berbagi pool.

Kombinasi dari beberapa faktor di atas biasanya yang bikin error ini muncul di jam-jam sibuk.

Langkah 1: Diagnosa Cepat dengan MySQL CLI

Jangan langsung ubah konfigurasi. Diagnosa dulu biar tahu akar masalahnya. Login ke server MySQL via terminal:


mysql -u root -p

Cek berapa koneksi yang sedang aktif dan berapa batasnya:


SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Max_used_connections';
SHOW VARIABLES LIKE 'max_connections';

Tiga query ini sudah cukup buat gambaran awal:

  • Threads_connected - jumlah koneksi yang sedang terbuka sekarang.
  • Max_used_connections - rekor tertinggi koneksi sejak server start. Kalau angkanya mentok di 151, itu tandanya kamu sudah pernah kena limit.
  • max_connections - batas maksimum yang diizinkan.

Terus, lihat siapa saja yang connect dan lagi ngapain:


SHOW FULL PROCESSLIST;

Perhatikan kolom Command. Kalau banyak baris dengan status Sleep, berarti koneksi nganggur yang numpuk. Kalau banyak Query dengan Time besar, berarti ada query lambat yang menahan koneksi.

Kadang MySQL sudah dalam kondisi kritis dan kamu tidak bisa login sama sekali karena semua slot koneksi habis. Tenang, MySQL menyisakan satu slot khusus untuk user dengan privilege CONNECTION_ADMIN (MySQL 8) atau SUPER (MySQL 5.7 ke bawah). Root biasanya punya privilege ini, jadi login sebagai root tetap bisa.

Langkah 2: Cari Connection Leak di Kode Aplikasi

Penyebab nomor satu yang saya temui adalah connection leak. Di PHP, misalnya, kalau kamu pakai mysqli atau PDO dan lupa menutup koneksi, koneksi itu tetap hidup sampai script selesai. Untuk request pendek ini biasanya tidak masalah, tapi untuk aplikasi dengan background process atau long-running script, ini jadi bom waktu.

Contoh kode PHP yang bocor:


<?php
// KODE BERMASALAH: koneksi tidak ditutup di semua jalur
function getUser($id) {
    $db = new mysqli('localhost', 'appuser', 'password', 'kinara_project');
    $result = $db->query("SELECT * FROM users WHERE id = " . $id);
    if ($result->num_rows === 0) {
        return null; // Lupa $db->close() di sini!
    }
    $row = $result->fetch_assoc();
    $db->close();
    return $row;
}
?>

Kalau return null dieksekusi, koneksi tidak pernah ditutup. Di PHP-FPM, setiap worker bisa menyimpan koneksi ini sampai worker-nya di-recycle. Kalau ada 50 worker PHP-FPM dan masing-masing nyangkut 2 koneksi, itu sudah 100 koneksi dari satu aplikasi.

Versi yang lebih aman - pakai try/finally atau pastikan semua jalur menutup koneksi:


<?php
function getUser($id) {
    $db = new mysqli('localhost', 'appuser', 'password', 'kinara_project');
    try {
        $stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
        $stmt->bind_param("i", $id);
        $stmt->execute();
        $result = $stmt->get_result();
        return $result->fetch_assoc();
    } finally {
        $db->close();
    }
}
?>

Untuk aplikasi Node.js, masalah yang sama muncul kalau kamu bikin koneksi baru di setiap request tanpa pool. Pakai mysql2 dengan pool:


const mysql = require('mysql2/promise');
const pool = mysql.createPool({
    host: 'localhost',
    user: 'appuser',
    password: 'password',
    database: 'kinara_project',
    connectionLimit: 10,
    waitForConnections: true,
    queueLimit: 0
});
// Pool otomatis me-reuse koneksi, tidak bikin baru terus-terusan

Prinsipnya sama di bahasa apapun: jangan bikin koneksi baru kalau bisa reuse. Connection pool adalah jawaban standar untuk masalah ini.

Langkah 3: Setel Timeout agar Koneksi Tidur Cepat Mati

Koneksi yang sudah tidak dipakai tapi statusnya Sleep tetap menghabiskan slot. MySQL menutup koneksi nganggur setelah wait_timeout detik. Defaultnya 28800 detik alias 8 jam. Untuk aplikasi web, 8 jam itu terlalu lama.

Setel di file konfigurasi MySQL (/etc/mysql/mysql.conf.d/mysqld.cnf di Ubuntu, atau /etc/my.cnf di CentOS):


[mysqld]
max_connections = 300
wait_timeout = 60
interactive_timeout = 300

Penjelasan singkat:

  • wait_timeout = 60 - koneksi non-interaktif (dari aplikasi) yang nganggur lebih dari 60 detik akan ditutup otomatis.
  • interactive_timeout = 300 - koneksi dari mysql CLI (interaktif) diberi waktu lebih lama, 5 menit.
  • max_connections = 300 - batas koneksi dinaikkan, tapi jangan asal gede, lihat bagian selanjutnya.

Restart MySQL setelah mengubah konfigurasi:


sudo systemctl restart mysql

Perhatian: jangan set wait_timeout terlalu kecil (misalnya 10 detik). Koneksi pool yang sedang dipakai bisa putus di tengah transaksi dan memicu error lain seperti MySQL server has gone away. 60 detik adalah titik aman untuk kebanyakan aplikasi web.

Langkah 4: Naikkan max_connections dengan Perhitungan yang Benar

Naikkan max_connections itu boleh, tapi setiap koneksi makan memory. MySQL mengalokasikan thread stack, buffer, dan struktur internal untuk tiap koneksi. Estimasi kasar: 1-2 MB per koneksi untuk konfigurasi default, bisa lebih kalau sort_buffer_size dan join_buffer_size digedein.

Rumus sederhananya:


# Memory per koneksi (perkiraan)
# = thread_stack + sort_buffer_size + join_buffer_size + read_buffer_size + ...
# Contoh MySQL 8 default:
# 256KB + 256KB + 256KB + 128KB + overhead ~ 1-2MB

# VPS dengan RAM 4GB, buffer pool 1GB, sisanya 3GB untuk koneksi:
# 3GB / 2MB = sekitar 1500 koneksi maksimal teoritis

Jangan pernah set max_connections = 10000 di VPS 2GB. MySQL bisa crash karena kehabisan memory (OOM) dan di-kill kernel. Naikkan bertahap: kalau sekarang 151 dan butuh 300, set 300, pantau Max_used_connections selama seminggu, baru naikkan lagi kalau perlu.

Cara mengubah sementara tanpa restart (untuk kondisi darurat):


SET GLOBAL max_connections = 500;

Tapi ingat, perubahan ini hilang setelah MySQL restart. Untuk permanen, tulis di file konfigurasi.

Langkah 4.5: Cek max_user_connections dan File Descriptor

Ada dua batasan lain yang sering bikin bingung karena gejalanya mirip. Pertama, max_user_connections. Ini batas koneksi per user. Defaultnya 0, artinya tidak ada batasan khusus - user cuma dibatasi oleh max_connections global. Tapi kalau admin sebelumnya pernah set nilai tertentu, satu user bisa kena limit duluan padahal server masih longgar:


SHOW VARIABLES LIKE 'max_user_connections';

Kalau nilainya bukan 0 dan aplikasi kamu kena User 'appuser' already has more than 'max_user_connections' active connections, naikkan atau set ke 0 (unlimited):


SET GLOBAL max_user_connections = 0;

Kedua, batas file descriptor di level OS. Setiap koneksi MySQL butuh satu file descriptor, dan sistem operasi punya batasnya sendiri. Kalau ulimit -n di server kecil, MySQL tidak akan bisa membuka koneksi sebanyak max_connections yang kamu set. Cek dengan:


# Batas file descriptor proses MySQL
cat /proc/$(pgrep mysqld | head -1)/limits | grep "open files"

Kalau batasnya 1024 atau 4096, naikkan di systemd unit MySQL dengan LimitNOFILE=65535, lalu restart. Error Too many open files di log MySQL adalah tanda jelas kamu kena masalah ini.

Langkah 5: Connection Pooling dengan ProxySQL

Kalau aplikasi kamu banyak dan masing-masing bikin koneksi sendiri, solusi jangka panjangnya adalah connection pool di sisi database. ProxySQL adalah proxy MySQL yang populer dan gratis. ProxySQL menerima koneksi dari aplikasi, lalu mem- multiplex-nya ke beberapa koneksi ke MySQL asli.


# Install ProxySQL di Ubuntu
sudo apt install proxysql
sudo systemctl enable proxysql
sudo systemctl start proxysql

# Masuk ke admin interface ProxySQL
mysql -u admin -padmin -h 127.0.0.1 -P 6032

Konfigurasi dasar - daftarkan MySQL server dan buat user monitoring:


-- Di admin interface (port 6032)
INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (10, '127.0.0.1', 3306);
INSERT INTO mysql_users (username, password, default_hostgroup) VALUES ('appuser', 'password', 10);
LOAD MYSQL SERVERS TO RUNTIME;
LOAD MYSQL USERS TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
SAVE MYSQL USERS TO DISK;

Setelah ini, aplikasi cukup connect ke ProxySQL di port 6033. ProxySQL membatasi jumlah koneksi nyata ke MySQL dan mengantre request yang berlebih. Efeknya, aplikasi yang tadinya butuh 500 koneksi cukup pakai 50 koneksi ke MySQL.

Untuk PostgreSQL, padanannya adalah PgBouncer. Konsepnya sama: antre dan reuse koneksi.

Langkah 6: Monitoring Supaya Tidak Kejadian Lagi

Error ini biasanya muncul tiba-tiba dan hilang sendiri, jadi sulit dilacak kalau tidak ada monitoring. Saya sarankan pasang mysqld_exporter (Prometheus exporter untuk MySQL) dan visualisasikan di Grafana. Metrik yang wajib dipantau:

  • mysql_global_status_threads_connected - jumlah koneksi aktif, pasang alert di 80% dari max_connections.
  • mysql_global_status_max_used_connections - rekor tertinggi, buat evaluasi kapasitas.
  • mysql_global_status_aborted_connects - koneksi gagal, kalau naik drastis berarti ada yang salah.
  • mysql_global_status_slow_queries - query lambat yang menahan koneksi.

# Contoh alert rule Prometheus: koneksi hampir penuh
# (mysql_global_status_threads_connected / mysql_global_variables_max_connections) * 100 > 80

Dengan alert ini, kamu dapat notifikasi sebelum error muncul, bukan setelah aplikasi mati. Pengalaman saya: alert 80% kasih waktu 5-10 menit buat cek sebelum benar-benar penuh.

Studi Kasus: Aplikasi PHP dengan 100 Worker FPM

Biar makin jelas, ini kasus nyata yang pernah saya tangani. Aplikasi Laravel di VPS 4GB dengan PHP-FPM 100 worker. Gejalanya: setiap jam 9 pagi, error 1040 muncul selama 5-10 menit, lalu hilang sendiri.

Diagnosa pertama: SHOW PROCESSLIST menunjukkan 140-an koneksi, dengan 80% status Sleep. Totalnya melewati 151, jadi koneksi baru ditolak. Hitung-hitungannya sederhana: 100 worker PHP-FPM, tiap worker menyimpan 1-2 koneksi ke database. Belum termasuk cron dan queue worker. Jadi 151 slot habis sebelum jam sibuk benar-benar mulai.

Yang saya lakukan, urut:

  1. Set wait_timeout = 60 - koneksi Sleep langsung dipangkas, 140 koneksi turun jadi 50-an dalam beberapa menit.
  2. Pasang connection pool di aplikasi (pakai persistent connection yang dibatasi) - worker FPM tidak lagi bikin koneksi baru tiap request.
  3. Naikkan max_connections dari 151 ke 300 sebagai buffer.
  4. Pasang alert di Grafana untuk threads_connected di 80%.

Hasilnya, error hilang dan pemakaian koneksi stabil di angka 60-80, jauh di bawah batas. Kuncinya bukan cuma menaikkan limit, tapi menghilangkan koneksi yang sebenarnya tidak perlu.

Tabel Error dan Solusi Cepat

Buat referensi cepat, ini pola error yang sering muncul dan solusinya:

  • Error 1040 Too many connections - batas koneksi tercapai. Cek Threads_connected, tutup koneksi Sleep, atau naikkan max_connections.
  • MySQL server has gone away - koneksi diputus server, biasanya karena timeout atau max_allowed_packet kecil. Naikkan wait_timeout atau max_allowed_packet.
  • Connection refused - MySQL tidak menerima koneksi, cek apakah service hidup dan port 3306 terbuka.
  • Access denied for user - salah user atau password, atau user dicabut privilege-nya.
  • Deadlock found - dua transaksi saling tunggu. Optimasi query dan index, atau kecilkan transaksi.

Simpan daftar ini, karena error database yang satu sering menyeret error yang lain.

Kesimpulan

Error "Too many connections" bukan berarti server kamu jelek. Ini hampir selalu soal manajemen koneksi. Urutan penanganan yang saya rekomendasikan:

  1. Diagnosa dengan SHOW PROCESSLIST dan SHOW STATUS - cari koneksi Sleep dan query lambat.
  2. Perbaiki connection leak di kode, pakai connection pool.
  3. Set wait_timeout yang wajar (60 detik).
  4. Naikkan max_connections bertahap dengan perhitungan memory.
  5. Kalau aplikasi banyak, pasang ProxySQL sebagai connection pool.
  6. Pasang monitoring biar dapat alert sebelum kena lagi.

Dari pengalaman saya, langkah 1 dan 2 menyelesaikan 80% kasus. Langkah 3 sampai 6 itu pengaman tambahan. Jangan lupa, perubahan konfigurasi selalu diuji dulu di staging, apalagi kalau menyangkut max_connections yang berhubungan langsung dengan memory server.

Kamu pernah kena error ini? Kasus kamu yang mana - connection leak, traffic spike, atau koneksi Sleep yang numpuk? Cerita di kolom komentar, siapa tahu pengalamanmu bisa nolong developer lain yang lagi panik gara-gara aplikasi mati jam segini.


Bagikan artikel ini:

Komentar (0)

Belum ada komentar. Jadilah yang pertama memberikan tanggapan!

Tinggalkan Komentar