Introduction

Node.js EventEmitter defaults to a maximum of 10 listeners per event. When more listeners are added, it emits a MaxListenersExceededWarning. This is a safety mechanism - in most cases, adding more than 10 listeners indicates a bug where listeners are being added in a loop without being removed, causing a memory leak.

Symptoms

  • (node:12345) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 data listeners added
  • Warning includes Use emitter.setMaxListeners() to increase limit
  • Memory usage grows over time as listeners accumulate
  • Process eventually runs out of memory
  • Warning triggered by streams, HTTP servers, or custom event emitters
bash
(node:12345) MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
11 error listeners added to [Server].
Use emitter.setMaxListeners() to increase limit
    at _addListener (node:events:587:17)
    at Server.addListener (node:events:605:10)

Common Causes

  • Adding event listeners inside a loop without removing old ones
  • Creating a new listener for each HTTP request on a shared emitter
  • Not removing listeners when components unmount or disconnect
  • Third-party libraries adding listeners without cleanup
  • Re-subscribing to events after reconnection without unsubscribing first

Step-by-Step Fix

  1. 1.Find the source of listener accumulation:
  2. 2.```bash
  3. 3.# Run with verbose warnings
  4. 4.NODE_OPTIONS="--trace-warnings" node app.js

# Or in code process.on('warning', (warning) => { console.warn('Warning stack:', warning.stack); }); ```

  1. 1.Remove listeners when no longer needed:
  2. 2.```javascript
  3. 3.// WRONG - listeners accumulate
  4. 4.connections.forEach(conn => {
  5. 5.server.on('close', () => conn.cleanup()); // New listener each time
  6. 6.});

// CORRECT - use named function for removal function handleServerClose() { connections.forEach(conn => conn.cleanup()); } server.on('close', handleServerClose);

// Or use once() for single-execution listeners server.once('close', () => { connections.forEach(conn => conn.cleanup()); }); ```

  1. 1.Increase limit when legitimately needed:
  2. 2.```javascript
  3. 3.const EventEmitter = require('events');
  4. 4.const emitter = new EventEmitter();

// Increase for this specific emitter emitter.setMaxListeners(50);

// Or set globally (not recommended) EventEmitter.defaultMaxListeners = 50;

// Or set to 0 for unlimited (use with caution) emitter.setMaxListeners(0); ```

  1. 1.Use listener count monitoring:
  2. 2.```javascript
  3. 3.const EventEmitter = require('events');

class MonitoredEmitter extends EventEmitter { on(event, listener) { const count = this.listenerCount(event); if (count > 5) { console.warn(Warning: ${count} listeners already on '${event}'); } return super.on(event, listener); } }

const emitter = new MonitoredEmitter(); ```

  1. 1.Clean up listeners with AbortController (Node.js 15+):
  2. 2.```javascript
  3. 3.const controller = new AbortController();

// All listeners attached with this signal are removed together emitter.on('data', handler1, { signal: controller.signal }); emitter.on('data', handler2, { signal: controller.signal }); emitter.on('error', errorHandler, { signal: controller.signal });

// Remove ALL listeners at once controller.abort(); // All three listeners removed cleanly ```

Prevention

  • Always use once() instead of on() for single-execution listeners
  • Remove listeners in cleanup/destroy methods
  • Use AbortController for batch listener management (Node 15+)
  • Monitor emitter.listenerCount(event) in production
  • Set --trace-warnings in development to catch issues early
  • Never blindly increase defaultMaxListeners without understanding why
  • Use events.on() async iterator pattern instead of emitter.on() for streams