Dalam ekosistem runtime Node.js, pola komunikasi asynchronous berbasis EventEmitter adalah tulang punggung dari hampir seluruh pustaka jaringan, streaming data, hingga pustaka WebSocket. Kemudahan mendaftarkan handler event melalui metode emitter.on('event', callback) membuat pola pub/sub ini diadopsi secara luas di berbagai lapisan arsitektur microservices modern.

Namun, di balik kepraktisan sintaksisnya, EventEmitter menyimpan potensi malapetaka performa yang kerap diabaikan: kebocoran memori (memory leak) bertahap akibat akumulasi listener yang tidak pernah dilepas. Masalah ini semakin berbahaya ketika pengembang dengan sengaja mematikan peringatan sistem bawaan menggunakan emitter.setMaxListeners(0) demi membungkam peringatan MaxListenersExceededWarning di konsol log, tanpa menyadari bahwa keputusan tersebut secara efektif menutupi bom waktu konsumsi RAM di server produksi.

Anatomi Kebocoran Heap dan Perangkap Closures

Memahami akar kebocoran memori pada Node.js menuntut analisis mendalam terhadap cara mesin V8 mengelola siklus hidup objek di memori heap:

  • Siklus Hidup Objek yang Tidak Seimbang: Masalah kebocoran memori terjadi ketika sebuah objek berumur panjang (long-lived subject), seperti instance singleton koneksi database, bus pesan internal, atau server HTTP global, mengikat callback dari objek berumur pendek (short-lived object), seperti instance request HTTP per pengguna. Setiap kali handler didaftarkan dengan emitter.on(), instance emitter akan menyimpan referensi kuat (strong reference) ke fungsi callback tersebut di dalam array listener internalnya. Selama emitter induk tetap hidup di memori, seluruh fungsi callback yang terdaftar tidak akan pernah disentuh oleh siklus Garbage Collection (GC).
  • Penjara Konteks Leksikal Melalui Closure: Bahaya sesungguhnya bukan sekadar ukuran memori fungsi callback itu sendiri, melainkan konteks leksikal yang ditangkap (captured closure). Ketika callback merujuk variabel di cakupan luarnya—misalnya payload request berukuran megabita, buffer data sesi, atau objek respons stream—seluruh rantai objek raksasa tersebut akan terus tertahan di memori. Dalam skenario server dengan ribuan request per detik, retensi ratusan ribu closure dalam beberapa jam sudah cukup untuk menguras habis ruang heap V8 hingga memicu crash fatal Fatal process out of memory: JavaScript heap out of memory.
  • Ilusi Keamanan emitter.once(): Banyak insinyur berasumsi bahwa beralih ke emitter.once() sepenuhnya menyelesaikan masalah. Asumsi ini salah fatal jika event target yang ditunggu tidak pernah dipancarkan (never emitted), misalnya akibat kegagalan timeout jaringan atau pembatalan request oleh klien sebelum stream rampung. Listener once tersebut akan tetap bersarang abadi di dalam memori heap, terus mengunci referensi closure tanpa batas waktu.

Strategi Penanganan Disiplin dan Modernisasi Listener

Mencegah kebocoran memori pada Event Emitter menuntut disiplin siklus hidup (lifecycle management) yang terukur dan penerapan fitur modern Node.js:

  1. Adopsi AbortSignal Bawaan pada Event Listener: Sejak Node.js v15 ke atas, metode emitter.on() dan events.on() secara resmi mendukung opsi signal dari AbortController. Dengan mengaitkan listener ke AbortSignal milik request HTTP atau batas waktu (timeout), handler akan terlepas secara otomatis dan bersih seketika siklus request berakhir atau dibatalkan, mengeliminasi kebutuhan kode pembersihan manual yang rawan terlewat:
const ac = new AbortController();
globalEventBus.on('dataUpdate', handleUpdate, { signal: ac.signal });
// Ketika request selesai atau klien terputus:
ac.abort();
  1. Kewajiban Pasangan Berimbang addListener dan removeListener: Jika menggunakan arsitektur warisan atau pola kelas kustom, terapkan prinsip kebersihan absolut. Setiap pemanggilan on atau addListener wajib memiliki padanan removeListener atau off di dalam blok finally penanganan stream atau pada event penutupan koneksi (socket.on('close')). Hindari penggunaan fungsi anonim (anonymous arrow function) sebagai callback jika Anda bermaksud melepasnya di kemudian hari, karena removeListener membutuhkan referensi identitas fungsi yang identik.
  2. Observabilitas Heap Aktif dan Profiling Berkelanjutan: Jangan menunggu crash OOM terjadi di lingkungan produksi. Pasang pemantauan metrik process.memoryUsage().heapUsed di dashboard observabilitas Prometheus atau Datadog. Ambil snapshot memori heap berkala menggunakan modul bawaan v8.getHeapSnapshot() atau Chrome DevTools Protocol saat terjadi tren kenaikan memori searah, lalu telusuri pohon retensi (retainer tree) untuk menemukan nama array listener yang membengkak secara anomali.

Arsitektur backend yang tangguh tidak hanya diuji dari kemampuannya menangani beban puncak, melainkan dari kedisiplinannya membersihkan jejak komputasi setelah tugas selesai dijalankan.