Node.js — MaxListenersExceededWarning on a Growing EventEmitter
Investigate MaxListenersExceededWarning: possible EventEmitter memory leak, find who adds listeners per request, and fix the registration and removal lifecycle.
Symptoms
- (node:4217) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 hello listeners added
- The warning count grows with request volume, and RSS creeps up alongside it
- Only one named event shows the warning while other events stay quiet
Possible causes
- A per-request handler subscribes to a shared emitter and never removes itself
- An error handler is attached inside a loop that re-runs on retry or reconnect
- on is used where once was intended, so listeners accumulate after first use
- A wrapper function adds a listener on every call and only removes it on the success path
Diagnosis steps
Get the exact registration stack
emitter.setMaxListeners(0) # temporarily, then run with --stack-trace-limit=50Interpretation
With the limit disabled, each warning prints the stack of the offending addListener call, naming the file and line that registers repeatedly.
Count listeners per event programmatically
setInterval(() => console.log(emitter.listenerCount('hello')), 5000)Interpretation
A count that only ever increases confirms a missing removal path; a count that oscillates means removal exists but leaks on one branch.
Grep for the event name
grep -R "on(.hello.\|once(.hello." src/Interpretation
Matching call sites in middleware, loops, or retry wrappers identify the accumulative registration.
Root cause
Listeners were added to a long-lived emitter more often than they were removed, so the emitter retained every closure together with everything it captured. The warning fires at 11 listeners precisely because retention, not the count itself, is the memory hazard.
Fix
Register listeners at the owner's lifecycle boundaries, remove them on completion with removeAllListeners or off in a finally block, use once for single-shot events, and pass AbortSignal to event helpers so cancellation removes subscriptions automatically.
Risk note
Calling setMaxListeners(0) permanently silences the detector without removing the retained closures. Treat it as a diagnostic tool only.
Verify the fix
Drive a load of a few thousand requests, confirm listenerCount returns to its baseline after completion, and confirm the warning never appears with default limits.
Symptom walkthrough
The warning includes the event name, which narrows the search instantly:
(node:4217) MaxListenersExceededWarning: Possible EventEmitter
memory leak detected. 11 hello listeners added to [EventEmitter].
Use emitter.setMaxListeners() to increase limit
Track the count over time to classify the leak:
setInterval(() => {
console.log('hello listeners:', emitter.listenerCount('hello'));
}, 5000);
Monotonic growth means no removal path exists. Sawtooth growth means removal exists but is skipped on one branch, usually the error path. Temporarily raising the limit to zero makes each warning print the registration stack, which points at the exact line.
Fix in practice
Move registration to a lifecycle boundary and mirror it with removal:
function withHello(onHello) {
emitter.on('hello', onHello);
return () => emitter.off('hello', onHello); // disposer
}
const dispose = withHello(handle);
try {
await job();
} finally {
dispose(); // runs on success and failure alike
}
For single-shot semantics, replace on with once. On modern Node, prefer the signal-based API which removes the listener when the signal aborts:
emitter.once('hello', handler, { signal: abortController.signal });
Verification
After the fix, run a few thousand requests through the path that previously accumulated listeners. listenerCount must return to baseline when the batch completes, including when you inject failures to exercise the error branch. Keep the default limit in place: the warning staying silent under load is the regression test for this class of leak. If a genuinely long-lived component needs more than ten listeners by design, raise the limit for that instance only and comment why.