← Debugging Guides

阅读中文版本

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.

beginnerdebuggingnodejseventsmemory

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=50

Interpretation

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.