Server Kecilku Restart Sendiri Tiap 46 Jam, dan Penyebabnya Cuma Satu Listener yang Lupa Kuhapus

Selama delapan hari berturut-turut, proses relay webhook kecilku mati sendiri setiap kira-kira 46 jam — bukan crash berisik dengan stack trace merah di terminal, tapi systemd yang diam-diam me-restart begitu RAM menyentuh limit 512MB yang kupasang sendiri di unit file-nya. Aku tahu ini terjadi karena journalctl mencatatnya rapi, tapi selama delapan hari itu juga aku menganggapnya bukan bug, melainkan bukti bahwa safety net-ku bekerja seperti seharusnya.

Kenapa Aku Butuh Delapan Hari untuk Curiga

Proyek ini kecil: sebuah relay yang menerima webhook dari beberapa penyedia pembayaran, memverifikasi tanda tangannya, lalu meneruskannya ke dua-tiga endpoint tujuan sambil mencatat status pengiriman. Trafiknya tidak besar, jadi ketika systemctl status menunjukkan restart tiap dua hari, aku menandainya sebagai "known behavior, memang begitu setelan memory limit-nya" dan pindah ke pekerjaan lain. Yang membuatku berhenti berasumsi bukan restart itu sendiri, tapi satu klien yang komplain lewat email: sekali dalam beberapa hari, satu webhook mereka terlambat diteruskan sampai tiga menit — persis di jendela waktu sebelum proses di-restart, saat event loop mulai tersendat karena garbage collector bekerja lebih keras dari biasanya.

Peringatan Kecil yang Kuabaikan di Log

Begitu aku benar-benar duduk membaca log lengkap, bukan cuma baris restart-nya, aku menemukan sesuatu yang sebenarnya sudah muncul sejak minggu pertama: (node:1) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 finish listeners added to [ServerResponse]. Node secara default memang mencurigai kebocoran begitu satu emitter punya lebih dari sepuluh listener untuk event yang sama, dan peringatan itu sudah tercetak ratusan kali di log produksi — tapi karena levelnya cuma warning, bukan error, dan proses tetap jalan normal setelahnya, aku selalu menutup terminal sebelum sempat menggulir ke atas untuk membacanya.

Satu addEventListener yang Terus Menumpuk, Tidak Pernah Dilepas

Akar masalahnya ternyata sesederhana yang paling kutakuti: di dalam handler webhook utama, aku memasang res.on('finish', () => bus.emit('delivery', payload)) untuk melaporkan status pengiriman ke sebuah EventEmitter global yang dipakai dashboard internal — tapi listener itu didaftarkan langsung ke objek req/res bawaan Express, bukan sekali di level middleware, dan tidak pernah dilepas dengan removeListener setelah response selesai. Untuk satu request, itu bukan masalah; res akan digarbage-collect begitu koneksi ditutup. Masalahnya muncul karena aku juga menyimpan referensi tiap res ke sebuah array pendingDeliveries untuk keperluan retry manual, dan lupa membersihkannya begitu status pengiriman sukses — jadi setiap res, beserta seluruh listener yang menempel padanya, tetap hidup di memori selama proses berjalan. Dengan rata-rata 40-an webhook per jam, array itu tumbuh terus, dan RAM naik nyaris linear sampai menyentuh limit systemd.

Baris-baris kode HTML di layar
Baris-baris kode HTML di layar

Aku memverifikasinya dengan cara paling telanjang: jalankan proses yang sama secara lokal dengan node --inspect, buka Chrome DevTools, ambil heap snapshot begitu start lalu satu lagi setelah menembakkan 500 webhook palsu lewat skrip curl berulang. Diff dua snapshot itu menunjukkan objek ClientRequest/ServerResponse yang jumlahnya nyaris sama persis dengan jumlah request yang baru kukirim — bukti langsung bahwa objek itu memang tidak pernah dilepas, bukan cuma dugaan dari pola RAM di grafik monitoring.

Apa yang Kuubah Supaya Ini Tidak Terulang Diam-Diam

Fix-nya sendiri kecil: hapus referensi dari pendingDeliveries begitu status pengiriman final tercatat, dan pindahkan pendaftaran listener ke satu tempat yang jelas siklus hidupnya alih-alih menempel ke tiap objek response. Tapi perubahan yang menurutku lebih penting ada di infrastruktur monitoring-nya: sekarang ada heartbeat tiap lima menit yang mengirim process.memoryUsage().rss ke dashboard yang sama dengan status pengiriman webhook, dengan alert kalau kenaikannya konsisten lebih dari 5% per jam selama tiga jam berturut-turut — bukan cuma alert ketika proses benar-benar restart. Aku juga menambahkan satu baris di CI: server dijalankan di bawah clinic doctor sambil menerima beban sintetis selama dua menit sebelum setiap deploy ke staging, supaya tren memori yang mencurigakan kelihatan sebelum kode itu sempat hidup di produksi selama delapan hari tanpa ada yang sadar.

Foto hitam-putih kotak persegi panjang abu-abu di rak kawat
Foto hitam-putih kotak persegi panjang abu-abu di rak kawat

Takeaway

Bug ini tidak pernah menyebabkan downtime yang terlihat — justru karena systemd rajin merestart proses sebelum benar-benar kehabisan memori, semuanya terlihat baik-baik saja dari luar selama lebih dari seminggu. Yang membuatnya berbahaya bukan bug-nya sendiri, tapi seberapa nyaman rasanya membiarkan mekanisme self-healing menutupi gejala tanpa pernah menanyakan kenapa gejala itu ada. Peringatan MaxListenersExceededWarning sudah ada sejak hari pertama, cetak jelas di log — masalahnya bukan alatnya tidak memberi tahu, tapi aku belum punya kebiasaan menganggap warning level rendah sebagai sinyal yang layak digali, bukan cuma baris yang lewat begitu saja di terminal.

Sumber gambar: Ales Nesetril di Unsplash Sumber gambar: Florian Olivo di Unsplash Sumber gambar: imgix di Unsplash

The conversation 0

No comments yet. Start the conversation.

Comments are reviewed before appearing. Links are masked.