Production issues in Node.js applications can be challenging to diagnose, especially when different problems exhibit similar symptoms. To make troubleshooting easier, we’re introducing a Node.js Production Troubleshooting Series, where each article focuses on one specific production issue, explores its underlying causes, and demonstrates a practical, step-by-step approach to identifying the root cause.
An Event Listener Leak is one of those production problems that can be difficult to notice at first. CPU and heap usage may look normal, but the number of event listeners keeps growing. A common example is code that calls `emitter.on()` inside a loop or for every request, but never removes the listener with `removeListener()` or `off()`. Over time, these listeners keep accumulating. Eventually, you may see `MaxListenersExceededWarning`, increased memory usage, or callbacks being triggered more times than expected.
In this blog, we’ll walk through how an Event Listener Leak shows up in a Node.js application, what typically causes it, and how to trace it back to the exact root cause. We’ll also simulate the issue in a sample application, capture diagnostic data as it happens, and analyze it step by step to pinpoint what’s going wrong.
What is an ‘Event Listener Leak’ in Node.js?
An Event Listener Leak occurs when listeners keep getting added to a long-lived `EventEmitter` and are never removed, so the number of attached listeners grows for the life of the process.
`EventEmitter.on()` (or `addListener()`) adds a function to an emitter. You can later remove it using `removeListener()` or `off()`. For more details, see the Node.js EventEmitter documentation.
The problem starts when a listener is added but never removed. The function stays attached to the emitter, along with anything it references. If this happens on every request or every call, the emitter collects listeners, and the memory they hold can never be garbage collected. Node warns you at 11 listeners for the same event on one emitter with `MaxListenersExceededWarning`, but the app keeps running, and the leak continues.
What causes an ‘Event Listener Leak’ in Node.js?
Here is the usual list of causes for the ‘Event Listener Leak’ problem.
- on() inside a request or timer: A listener is added every time the code runs, but it is never removed when the request or operation finishes.
- Anonymous functions you cannot remove: For example, `emitter.on(‘data’, () => {})` creates a new function every time. Because there is no reference to the original function, it cannot easily be removed later with `off()`.
- setMaxListeners(0): This disables the listener limit warning, but it does not fix the leak. The listeners are still being added and continue to accumulate.
- Shared emitters: A module-level `EventEmitter` that every tenant or connection attaches to.
- Forgetting once(): A one-shot event registered with `on()` instead of `once()`, so it stays after the first fire.
How to Simulate Event Listener Leak in Node.js
To understand how an Event Listener Leak appears in the diagnostic data, let’s reproduce the issue using a sample Node.js application.
The following program adds 50 listeners every 500 ms on one `EventEmitter` and never removes them. `setMaxListeners(0)` is only there so Node does not stop the demo with a warning.
``` JS Code (listener-leak.js)const { EventEmitter } = require('events');class ListenerLeakDemo { constructor() { this.emitter = new EventEmitter(); this.intervalId = null; this.count = 0; } start() { if (this.intervalId) return; this.emitter.setMaxListeners(0); this.intervalId = setInterval(() => { for (let i = 0; i < 50; i++) { this.emitter.on('chaos-event', () => {}); } this.count += 50; this.emitter.emit('chaos-event'); console.log(`Listener leak: ${this.count} listeners`); }, 500); } stop() { clearInterval(this.intervalId); this.emitter.removeAllListeners(); this.count = 0; }}module.exports = new ListenerLeakDemo();```
How to Capture Diagnostic Data for Troubleshooting Listener Leaks
yCrash is a diagnostic tool that captures runtime performance data from a live Node.js process, including CPU usage, event loop lag, worker thread activity, and call stacks, and turns that data into a report that points to the root cause. To set up yCrash for your Node.js application, follow the steps below:
1. Install yCrash
yCrash is available in both Cloud and On-premises versions. Use one of the options below to get started:
- Cloud Service: Create an account to use the cloud version.
- On-premises: Register for a 14-day trial and install the yCrash application on your local machine or within the organization’s environment.
2. Set up the yCrash Node.js hook first
Please note that this is a one-time setup. The hook must be loaded into your Node.js application before yCrash can capture the Node.js internals data that matters for diagnosis, things like worker thread CPU, event loop lag, method profiling, and call stacks. To set this up, follow the Hook Mode steps here: Node.js Diagnostic Capture. This hook setup adds very minimalistic, almost zero overhead to your application.
3. Launch yc-360 script
Configure and start the yc-360 script to monitor your Node.js process. The full setup and configuration options specific to your deployment are available here: Micro-metrics Monitoring (M3) Mode
When the yc-360 script detects a problem, it captures 360° diagnostic artifacts (GC log, Event loop lags, Worker threads, CPU Profile, Unhandled rejections, Call stack, Active handles, Process, Storage, Kernel, Network, etc.). It uploads them to the yCrash server automatically. yCrash server analyzes the artifacts using advanced pattern recognition and ML algorithms to identify the root cause of the problem.
How to Analyze Event Listener Leak Using the Diagnostic Data
In this section, we’ll see how to analyze an Event Listener Leak using the diagnostic data captured by yCrash. We’ll start with the incident on the calendar dashboard, check the RCA Summary to understand what’s going on, and then open Active Handles to see EventListener climbing alone.
Step 1: Open the incident from the yCrash dashboard
When the yc-360 script detects any performance degradation in the Node.js application, it automatically captures 360° diagnostic data and transmits it to the yCrash server. The yCrash server analyses this data and creates an incident in the yCrash dashboard as shown in the screenshot below. Open that incident from the yCrash calendar dashboard to view the reports.

Fig: yCrash dashboard, incident list view
Step 2: Start with the RCA Summary Page
Before diving into details, let’s start with the RCA Summary. yCrash analyzes the Node.js diagnostic data and brings the key findings together in simple language with their severity levels.
In the RCA Summary, we will focus on two sections:
- AI Overview
- Issues in application and device
AI Overview
At the top of the RCA summary report page, you’ll see an AI Overview section. It gives you an AI-generated summary of the incident, which would make even a non-technical person understand the root cause of the problem. Within the AI Overview section, below the AI summary, you can also click on the ‘Dive Deeper in AI Mode‘ button to open a chat window to ask any question to the AI about this incident.

Fig: RCA Summary highlighting the AI Overview
As shown in the AI Overview above, the root cause is an event listener leak. Listeners increased by 25,119, from 35,888 to 61,007. CPU and memory on the host stay low. That is typical. Listeners are cheap until the count gets huge.
Issues in application and device

Fig: RCA Summary highlighting the issues in application and device
As shown in the above screenshot, 2 application-level issues and 1 device-level issue are reported. The one that matters for a Listener Leak is:
- Event listener leak
The ms package warning and the device note about 19% memory do not add listeners. App Memory on this capture is about 0% and App CPU is about 2%.
Step 3: Confirm EventListener is the only climbing line
Click “details here” link present at the end of Event listener leak problem on the RCA Summary page. It takes to the Active Handles & Requests Growth Trend section of the Node.js Internals report.

Fig: Active Handles showing EventListener climbing with Total
The banner repeats the Fatal listener leak. On the chart, `EventListener` sits on top of Total while Pipes, TCP servers, and timers stay at 1-3. That pattern points to an event listener leak, rather than a socket or file leak.
How to fix an Event Listener Leak in Node.js
The following are the potential solutions for this issue:
- If you add a listener with `on()` or `addListener()`, make sure you remove it with `removeListener()` or `off()` when it is no longer needed.
- If an event only needs to be handled once, use `once()` instead of `on()`. You can also use `{ once: true }` or an `AbortSignal` where appropriate. This lets the listener clean itself up automatically.
- If you need to remove a listener later, use a named function instead of creating an inline arrow function. `off()` needs the same function reference that was passed to `on()`.
- Do not use `setMaxListeners(0)` to hide the warning. This only removes the warning. It does not remove the listeners or fix the leak. The real fix is to find where listeners are being added and make sure they are removed at the right time.
Conclusion
Diagnosing and identifying the root cause of an Event Listener Leak is straightforward once Handle Growth is in front of you. In this example, the diagnostic data was captured using the yc-360 script and analyzed using the yCrash server, which showed EventListener climbing alone.
The analysis traced the issue to listeners added and never removed. The count rose by 25,119 in the capture window. Removing listeners when the request or connection ends addresses the underlying problem and helps prevent the issue from recurring.
