Node.js "MaxListenersExceededWarning" - Fix EventEmitter Leaks in CI
Node warns when an EventEmitter accumulates more than 10 listeners for one event - usually a sign that listeners are added in a loop or per request without ever being removed.
What this error means
A noisy MaxListenersExceededWarning appears in logs (and a stack trace if --trace-warnings is on). In long test runs it can precede slow growth in memory. The warning itself does not crash, but it points at a real leak.
(node:1) MaxListenersExceededWarning: Possible EventEmitter memory leak
detected. 11 SIGTERM listeners added to [process]. Use emitter.setMaxListeners()
to increase limitDiagnose it: the shell in CI is not your shell
Package scripts run under a different shell, a different PATH, and a non-interactive environment on a runner. Most scripts that fail only in CI are relying on something the login shell gave them locally: a tool on PATH, an environment variable from a dotfile, or a TTY.
# what the script can actually see
npm run env | grep -E "^(PATH|NODE_ENV|CI)="
# is the binary on PATH for the script, not just for you?
npm exec -- which <tool> || echo "not resolvable from npm scripts"
# run the exact script with tracing
sh -x -c "$(node -p "require('./package.json').scripts.build")"Common causes
Listeners added without removal
A function called repeatedly (per request, per test, in a loop) attaches a listener each time but never calls removeListener/off. The count climbs past the default limit of 10.
A genuinely high but bounded listener count
Some legitimate designs attach more than 10 listeners to one emitter on purpose. Here the warning is a false positive and the limit should be raised intentionally.
How to fix it
Find and remove the leaked listeners
Run with --trace-warnings to get the stack where the listener is added, then ensure each on() has a matching off() (or use once()).
node --trace-warnings app.mjs
# in code: remove what you add
const onTerm = () => shutdown();
process.on('SIGTERM', onTerm);
// later
process.off('SIGTERM', onTerm);Raise the limit only when the count is legitimate
If many listeners are correct by design, raise the threshold on that specific emitter - not globally as a way to silence a real leak.
emitter.setMaxListeners(50);Make failures fail the job
A multi-command script can report success while a middle command failed, which produces the worst kind of CI result: a green build that shipped something broken.
# pipefail is NOT set by default in every runner shell
- name: Build
shell: bash
run: |
set -euo pipefail
npm run build | tee build.logHow to prevent it
- Pair every
on()with anoff(), or useonce()for one-shot handlers. - Run with
--trace-warningsin CI so the leak source is visible. - Only call
setMaxListenerswhen a high count is intentional and bounded.