Node.js

Node.js — MaxListenersExceededWarning: possible EventEmitter memory leak

Written and reviewed by Sahil Srivastav

EventEmitterRetained closuresLifecycle cleanup
MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 data listeners added to [EventEmitter]. MaxListeners is 10. Use emitter.setMaxListeners() to increase limit

What this error actually means

An EventEmitter has more listeners for one event name than its configured warning threshold. The threshold is a diagnostic heuristic rather than a capacity limit: additional listeners are still registered. The message does not prove a leak, but it identifies a specific emitter and event whose ownership deserves inspection.

A long-lived emitter retains its listener functions. A listener closure can in turn retain the request object, buffers and services referenced by that closure. Even if the request has ended, garbage collection cannot free that graph while the listener remains registered. Duplicate registration also changes behaviour: a single event can trigger the same logical action several times.

The crucial comparison is listener count against live consumers. A hundred legitimate simultaneous subscribers can be healthy; eleven abandoned request listeners can be a leak. Counts should return to the expected baseline when consumers finish. Suppressing the warning without measuring that invariant removes the earliest evidence.

Causes, most common first

  1. 1A request subscribes to a process-wide emitter without cleanup. Each HTTP request adds a listener while only the success path removes it. Timeout, client disconnect and thrown exceptions leave registrations behind. The request finishes, but the emitter outlives it and keeps the closure reachable.
  2. 2Reconnect or initialisation repeats registration. A reconnect callback installs handlers already registered during startup. A module factory may also run once per request while referring to a shared singleton emitter. Track lifecycle transitions: a retry of setup should not multiply observers on the same resource.
  3. 3once waits forever for an event that never arrives. A once listener removes itself only when its event fires. If the operation is cancelled or the expected event never occurs, the listener can remain indefinitely. One-shot registration still needs cancellation and timeout cleanup.
  4. 4The component intentionally supports many concurrent consumers. A bounded subscriber set can legitimately exceed ten. Establish the maximum expected concurrency and show that counts fall when subscriptions end. An explicit per-emitter threshold may then be appropriate; a global unlimited setting cannot express this ownership contract.

When you see it

  • Warnings appear after a repeatable number of requests, reconnects or test cases
  • One emitted event produces duplicate callbacks or repeated notifications
  • Heap snapshots retain completed requests through an emitter’s listener array
  • Listener count remains elevated after clients disconnect or a test finishes

How to diagnose it

Step 1

Locate the registration that crossed the threshold

Warning traces point to the addition that triggered the warning, not necessarily the first leaked listener. Trace that call back to the request or reconnect lifecycle and inspect how all previous registrations were supposed to be removed.

node --trace-warnings server.js

Step 2

Measure listeners before and after an operation

Instrument the actual shared emitter with listenerCount(eventName), then complete, reject and cancel a consumer. A return to baseline is stronger evidence than absence of a warning. A small leak below the threshold remains a leak.

Step 3

Inspect the warning’s emitter without dumping request contents

The warning object exposes emitter, type and count for this warning category. Record the event name, count and a safe component identifier. Avoid serialising the whole emitter because listeners and attached metadata may contain sensitive application state.

Step 4

Check callback identity at deregistration

removeListener or off must receive the same function object used for registration. Two arrow functions with identical source are different references. Also check whether repeated registrations of the same function require corresponding removal operations.

The fix

Make subscription return an explicit cleanup function, and tie that function to the consumer’s full lifecycle. HTTP consumers need cleanup when the response finishes and when the connection closes; background consumers need cancellation and shutdown hooks. Make cleanup idempotent because completion and disconnect can race.

Keep the listener reference in the same scope as its removal. Do not reconstruct a bound function during cleanup: handler.bind(context) creates a new function each time. Store the bound function once if binding is required.

Move process-wide registrations into one startup path and make reconnect handlers replace or reuse existing subscriptions deliberately. If module initialisation can run more than once during tests or hot reload, provide a teardown that removes only that module’s own listeners.

Raise the threshold on a specific emitter only after demonstrating bounded, intentional fan-out. Never call removeAllListeners on a shared emitter as a shortcut; it can remove another component’s handlers and hide the leak by breaking the system.

function subscribeUntilResponseEnds(bus, res, onUpdate) {
  let cleaned = false;
  function cleanup() {
    if (cleaned) return;
    cleaned = true;
    bus.off("update", onUpdate);
    res.off("finish", cleanup);
    res.off("close", cleanup);
  }
  bus.on("update", onUpdate);
  res.once("finish", cleanup);
  res.once("close", cleanup);
  return cleanup; // Call this on any earlier setup failure too.
}

How to stop it coming back

  • Assert listener counts return to baseline after success, failure and cancellation
  • Track subscriptions alongside active consumers, especially after reconnect storms
  • Give every long-lived emitter a named owner and documented subscription lifecycle
  • Avoid raising EventEmitter.defaultMaxListeners globally to silence one component

Practise production debugging in a real repository

Reading about a failure and reproducing one are different skills. Gronex ships broken backend repositories with failing test suites that encode the real invariant, so you debug from evidence instead of memorising symptoms.

FAQ

Does Node refuse the eleventh listener?

No. The threshold emits a warning rather than preventing registration. Duplicate handlers can continue accumulating and executing, so application behaviour may degrade well before heap exhaustion.

Is once enough to avoid leaks?

Only when the event is guaranteed to occur before the consumer ends. Cancellation or a missing event leaves the one-shot listener registered. Pair once with explicit teardown for those paths.

Why does off with the same arrow function not work?

Function identity is by reference. Writing another arrow expression creates another object even if its body is identical. Store the function passed to on and pass that exact reference to off.

Related

Other errors engineers hit next to this one

Full error and symptom index →