CF
Engineering6 Juli 2026

Database Saya Reboot Sendiri Jam 5 Pagi, dan API-nya Nge-hang 30 Detik

Cong Fandi
Cong Fandi9 min read
...
Database Saya Reboot Sendiri Jam 5 Pagi, dan API-nya Nge-hang 30 Detik

Jadi ceritanya jam 5-an pagi HP saya belum bunyi apa-apa, saya masih tidur nyenyak. Yang bunyi justru chat user: "Bang, layar utamanya lemot banget, muter-muter doang terus nge-freeze."

Saya buka aplikasinya. Bener. Layar utama yang biasanya kebuka setengah detik sekarang beku sampai 30 detik penuh, baru ada yang gagal. Bukan error cepat yang bilang "ada yang rusak" — tapi beku diam-diam, kayak aplikasinya lagi mikir keras banget tapi nggak ngasih tau lagi mikirin apa.

Yang bikin saya penasaran: nggak ada yang saya deploy semalem. Nggak ada kode yang berubah. Database juga kecil, cuma 1.4GB. Terus kenapa tiba-tiba sekarat?

Ternyata jawabannya nggak ada hubungannya sama kode saya, dan nggak ada hubungannya sama ukuran database. Ini cerita lengkap debugging-nya, plus playbook yang saya tulis biar nggak panik lagi kalau kejadian ini balik lagi. 🕵️


Dulu, Kenali Dulu Panggung-nya

Sebelum masuk ke drama-nya, ini setup yang lagi kita omongin. Nggak ribet-ribet amat:

Arsitektur: client → load balancer → dua app node → MySQL → Redis

Bacanya dari atas: aplikasi mobile manggil beberapa endpoint sekaligus buat ngisi layar utama (tujuh panggilan paralel), masuk ke load balancer, dilempar ke dua app server yang jalan Node.js pakai PM2 cluster (dua proses per server, jadi total empat proses Node), semuanya nembak ke satu MySQL, ditemenin Redis buat cache.

Satu angka yang penting saya tekankan dari awal: database-nya kecil. 1.4GB, sekitar 10 juta baris. Buat MySQL itu bukan apa-apa. Jadi kalau ada yang bilang "wah kegedean tuh databasenya, ganti aja" — pegang dulu pikiran itu, karena bukan itu masalahnya. Sama sekali bukan.


Debugging: Ngikutin Jejak, Bukan Nebak

Pas API lemot, godaan terbesar itu langsung nebak. "Pasti query berat!" "Pasti perlu ganti Postgres!" "Pasti kena serangan!" Saya paksa diri buat nggak nebak dan ikutin jejaknya satu-satu.

Langkah 1 — Node-nya sendiri hidup nggak?

Cek dulu prosesnya sehat apa nggak:

pm2 list
pm2 show <app-name>    # status, restart, CPU, memori, event loop latency
pm2 monit              # monitoring live

Hasilnya bikin bingung sekaligus ngasih petunjuk: aplikasinya online dan santai. CPU 1%, event loop cuma 0.5ms. Artinya Node-nya nggak lagi kepayahan. Dia lagi nungguin sesuatu. Kalau Node idle tapi request lemot, berarti bottleneck-nya di luar Node — database, Redis, atau jaringan.

Langkah 2 — Endpoint mana yang lemot?

Daripada ngira-ngira, saya ukur tiap endpoint pakai loop curl yang nyatet timing:

TOKEN="<jwt-token>"
BASE="https://api.example.com"
for p in "/endpoint-a" "/endpoint-b" "/endpoint-c" "/endpoint-d" \
         "/endpoint-e" "/endpoint-f" "/endpoint-g"; do
  curl -s -o /dev/null \
    -w "%{http_code} ttfb=%{time_starttransfer} total=%{time_total}  $p\n" \
    --max-time 30 -H "Authorization: Bearer $TOKEN" "$BASE$p"
done

Ini yang keluar. Sebagian endpoint kenceng (~0.1s), sebagian mentok 30 detik penuh terus mati:

Hasil curl sebelum dan sesudah restart: sebagian endpoint nge-hang 30 detik, sisanya cepat

Perhatiin baik-baik angkanya. Yang macet itu ttfb=0 (waktu ke byte pertama nol) tapi total=30s. Terjemahannya: server nerima koneksinya, tapi nggak pernah ngirim satu byte pun sampai akhirnya timeout. Itu bukan "lambat" — itu nge-hang. Dan angka 30 detik itu bukan kebetulan, itu persis nilai acquire: 30000 di config pool saya. Petunjuk gede banget: request-nya nggak bisa dapet koneksi database.

Langkah 3 — Baca log error (di sinilah root cause-nya nongol)

pm2 logs <app-name> --err --lines 100 | grep -iE "acquire|timeout|connection|sequelize|econnrefused"

Dan di sana dia:

SequelizeConnectionRefusedError: connect ECONNREFUSED <DB_HOST>:3306

ECONNREFUSED. MySQL nolak koneksi. Oke, jadi sekarang saya punya tersangka. Tapi belum kelar — kenapa MySQL nolak? Dia mati? Kepenuhan? Kena OOM?

Langkah 4 — Periksa server database-nya

sudo systemctl status mysql       # hidup? baru restart? crash-loop?
sudo ss -tlnp | grep 3306         # listening di IP yang bener?
df -h ; df -i                     # disk penuh? (penyebab umum MySQL mati)
free -h                           # OOM? cek swap
sudo dmesg -T | grep -iE "oom|killed process|out of memory"
sudo tail -60 /var/log/mysql/error.log

Plot twist: pas saya cek, MySQL-nya malah sehat wal afiat. Disk 72%, RAM banyak nganggur, nggak ada jejak OOM, error log-nya kosong. MySQL nggak crash sendiri. Jadi kenapa tadi nolak koneksi?

Langkah 5 — Oh. Seluruh VM-nya yang reboot.

who -b                            # waktu boot sistem
last -x reboot shutdown | head    # riwayat reboot

Ini kunci teka-tekinya. Waktu system boot persis jatuh di jam insiden. Malah ada dua reboot beruntun, dan versi kernel-nya naik. Artinya bukan MySQL yang crash — VM-nya yang reboot, kemungkinan besar gara-gara unattended-upgrades yang auto-reboot habis nge-patch kernel. MySQL cuma ikut mati sebentar pas mesinnya restart.

Ini timeline lengkapnya begitu semua potongan ketemu:

Timeline insiden dari reboot database sampai pemulihan


Tapi Tunggu — Kalau DB-nya Udah Hidup Lagi, Kenapa Masih Lemot?

Nah ini bagian yang paling menarik, dan pelajaran terbesar dari seluruh insiden ini.

Database-nya cuma mati beberapa detik pas reboot. Pas saya lagi nge-debug, MySQL udah hidup sehat lagi berjam-jam. Tapi API-nya tetep nge-hang. Kok bisa DB sehat tapi aplikasi masih macet?

Jawabannya: connection pool di Node nyimpen koneksi yang udah mati.

Kenapa aplikasi tetap rusak setelah DB kembali: pool menyimpan koneksi mati

Gini logikanya, tiga langkah:

  1. DB blip. Pas VM reboot, MySQL mati beberapa detik. Semua koneksi TCP yang lagi kebuka ke situ langsung putus.
  2. Pool nyimpen mayat. Sequelize (lewat driver mysql2) punya pool koneksi, max: 10 per proses. Masalahnya, driver-nya nggak selalu otomatis buang koneksi yang udah rusak. Jadi socket-socket mati peninggalan reboot itu masih nangkring di pool, kelihatannya masih "available".
  3. Request kebagian mayat. Pas request masuk dan ambil salah satu koneksi mati itu, dia nggak langsung error. Dia nunggu. Nungguin koneksi yang nggak akan pernah nyaut, sampai mentok acquire: 30000 — 30 detik penuh — baru nyerah.

Jadi user ngeliat layar beku 30 detik bukan karena DB-nya lagi mati, tapi karena aplikasinya lagi sopan-sopan nungguin koneksi yang udah jadi hantu.

Dan config saya malah bikin makin parah: ada retry ECONNREFUSED 3×, connectTimeout 15 detik, acquire 30 detik. Semua angka "sabar" itu ngubah yang harusnya gagal cepat jadi nge-hang lama.


Ada Bonus Masalah: Disk Hampir Penuh

Selagi nyelidik, saya sekalian cek disk app server. Ternyata:

df -h
du -sh ~/.pm2/logs
sudo du -xh / --max-depth=1 | sort -rh | head

Disk-nya 94% dan 92% — hampir penuh. Biang keroknya? Log PM2 yang nggak pernah dirotasi. Numpuk diam-diam berbulan-bulan. Ini bukan penyebab insiden hari itu, tapi bom waktu yang lagi nunggu giliran meledak.


Yang Beneran Menyelesaikan Masalah

Setelah semua kelihatan jelas, solusinya justru simpel banget:

#AksiHasil
1pm2 flush di semua app serverDisk turun drastis (94% → 67%, bebas ~6GB)
2Pasang & set pm2-logrotate (max 20M, simpan 3, compress)Log nggak akan membengkak lagi
3pm2 restart allConnection pool ke-reset → semua endpoint normal
4Verifikasi ulang pakai loop curlSemua 200; yang tadi hang jadi sukses 100% ~0.1s

Kuncinya ada di langkah 3. Kenapa pm2 restart all berhasil? Restart bikin proses Node yang benar-benar baru, dengan koneksi database yang fresh. Semua koneksi mayat peninggalan reboot dibuang total. Jadi yang benar-benar nyembuhin itu reset pool-nya, bukan karena DB-nya yang salah, bukan karena kode saya yang salah.

Perhatiin bedanya: saya restart service (pm2), bukan reboot server. Itu penting.


Playbook: Kalau Kejadian Lagi

Biar nggak panik dan nggak nebak-nebak lagi, saya tulis urutan ini. Simpen buat kejadian API lemot / RTO berikutnya:

Playbook 8 langkah kalau API lambat atau timeout lagi

  1. App-nya hidup?pm2 list / pm2 monit. Kalau CPU & event loop rendah tapi request lemot = bottleneck di luar Node.
  2. Endpoint mana yang lemot? → jalanin loop curl timing. ttfb=0 + 30s = nge-hang nungguin DB.
  3. Baca error aplikasi → grep ECONNREFUSED / acquire timeout di pm2 logs.
  4. Cek server DBsystemctl status mysql, df -h, free -h, who -b, last -x reboot. Pastikan MySQL up, disk nggak penuh, dan cek apa VM baru reboot.
  5. Kalau DB beneran matisudo systemctl restart mysql (restart service, bukan reboot server), pastikan systemctl enable mysql.
  6. Kalau DB sehat tapi app tetep hangpm2 restart all buat reset connection pool. ⚠️ Ini yang nyelesain insiden ini.
  7. Cek disk app serverdf -h. Kalau >85%, pm2 flush + pastiin pm2-logrotate aktif.
  8. Verifikasi pakai loop curl lagi sampai semua 200.

Aturan emas: Jangan buru-buru reboot seluruh server DB kalau MySQL-nya sebenernya sehat — itu cuma bikin outage tanpa benerin apa-apa. Utamain restart service, bukan server.


PR Buat Diri Sendiri (Biar Nggak Terulang)

Insiden kelar bukan berarti selesai. Ini yang lagi saya kerjain biar nggak kejadian lagi:

  • Auto-recovery koneksi DB. Perkecil connectTimeout/acquire, turunin retry.max, biar aplikasi gagal cepat (1–2 detik) bukan nge-hang 30 detik. Tambah connection validation / health-check biar koneksi mati langsung dibuang.
  • Global request timeout (mis. 5–8 detik → balikin 503) biar nggak ada satu request pun yang bisa mbekuin user 30 detik.
  • Kendaliin reboot VM. Cek /etc/apt/apt.conf.d/50unattended-upgrades. Kalau auto-reboot nyala, jadwalin ke jam sepi atau patch manual.
  • Log rotation permanen di semua node, konsisten.
  • Monitoring & alerting. Ini insiden ketahuan dari laporan user, bukan dari alert — dan itu memalukan. Pasang uptime check ke MySQL + alert ke Discord/Slack begitu muncul ECONNREFUSED.

Pelajaran yang Saya Bawa Pulang

Kalau cuma boleh inget beberapa poin dari kejadian ini:

  1. App idle tapi request lemot = masalahnya di dependency (DB/Redis/jaringan), bukan di kode.
  2. ttfb=0 + timeout = nge-hang di server, biasanya lagi nungguin koneksi DB.
  3. DB reboot bisa ninggalin koneksi pool yang matipm2 restart all buat reset.
  4. Restart SERVICE dulu, jangan reboot SERVER kalau komponennya sebenernya sehat.
  5. Database kecil (1.4GB / 10jt baris) itu bukan masalah — jangan salah nyalahin MySQL, jangan gonta-ganti database gara-gara insiden kayak gini.
  6. Log tanpa rotasi = bom waktu yang diam-diam menuhin disk.

Poin nomor 5 itu yang paling pengen saya tekankan. Godaan pas panik itu selalu "ganti teknologinya, over-optimize semuanya". Padahal masalahnya sering kali bukan di situ. Kadang jawabannya cuma: koneksinya basi, restart aja biar fresh.

Kalau server kalian ada yang lemot mendadak sementara nggak ada yang di-deploy — coba cek dulu, jangan-jangan mesinnya baru reboot diam-diam dan aplikasinya masih meluk koneksi hantu. 🚀

Versi English ada di sini.

PostmortemNode.jsPM2MySQLSequelizeDevOpsDebugging