Introduction
Node.js MaxListenersExceededWarning fires when more than 10 listeners are attached to the same event on an EventEmitter. While the default limit of 10 is configurable, exceeding it is almost always a sign of a memory leak -- listeners are being added but never removed, causing the emitter to accumulate listeners indefinitely. In long-running server processes, this leads to increasing memory usage, slower event processing (each event triggers more listeners), and eventually out-of-memory crashes.
Symptoms
(node:12345) MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
11 data listeners added to [ReadStream]. Use emitter.setMaxListeners() to increase limitOr on process:
(node:12345) MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
15 uncaughtException listeners added to [process].Memory grows over time:
```bash # After 1 hour $ cat /proc/12345/status | grep VmRSS VmRSS: 256000 kB
# After 24 hours $ cat /proc/12345/status | grep VmRSS VmRSS: 1536000 kB # 1.5GB - growing! ```
Common Causes
- Adding listeners in a loop:
on()called inside a request handler or loop - Not removing listeners on cleanup: Event listeners added but never removed when no longer needed
- Multiple middleware adding same listener: Express middleware adds a listener on every request
- Reusing event emitter across requests: Shared emitter accumulates per-request listeners
- **Using
on()instead ofonce()**: When only one event handling is needed - Error handler accumulation: Adding
process.on('uncaughtException')in multiple modules
Step-by-Step Fix
Step 1: Find the leaking listener
```javascript // Increase limit temporarily to see the full stack trace const EventEmitter = require('events'); EventEmitter.defaultMaxListeners = 20;
// Use the --trace-warnings flag // node --trace-warnings server.js
// Or programmatically enable detailed warnings process.on('warning', (warning) => { console.warn('Warning name:', warning.name); console.warn('Warning stack:', warning.stack); }); ```
Step 2: Fix listener added in a loop or request handler
```javascript // WRONG - adds a new listener on every request app.get('/api/data', (req, res) => { db.on('change', (data) => { res.json(data); // New listener added per request, never removed }); db.fetch(); });
// CORRECT - use once for single-event handling app.get('/api/data', (req, res) => { db.once('change', (data) => { res.json(data); }); db.fetch(); });
// OR CORRECT - set up listener once, outside request handler db.on('change', (data) => { // Broadcast to all connected clients broadcastToClients(data); }); ```
Step 3: Remove listeners on cleanup
```javascript class Connection { constructor(db) { this.db = db; this.onChange = this.handleChange.bind(this); }
connect() { this.db.on('change', this.onChange); }
disconnect() { // IMPORTANT: Remove listener when disconnecting this.db.removeListener('change', this.onChange); }
handleChange(data) { console.log('Data changed:', data); } }
// Usage const conn = new Connection(db); conn.connect();
// When done conn.disconnect(); // Listener properly removed ```
Step 4: Use AbortSignal for automatic cleanup (Node.js 15+)
```javascript const { on } = require('events');
async function monitorDatabase(db) { const controller = new AbortController();
try { // Events are automatically cleaned up when the controller is aborted for await (const [data] of on(db, 'change', { signal: controller.signal })) { console.log('Database changed:', data); if (shouldStop(data)) { controller.abort(); // Cleans up the listener break; } } } catch (err) { if (err.name === 'AbortError') { console.log('Monitoring stopped'); } else { throw err; } } } ```
Prevention
- Use
emitter.once()instead ofemitter.on()for single-event handling - Always store listener function references so they can be removed with
removeListener() - Use
AbortSignalfor automatic listener cleanup in modern Node.js - Never add event listeners inside request handlers or loops
- Monitor listener count with
emitter.listenerCount('eventName')in production - Add a health check that warns when any EventEmitter exceeds 8 listeners
- Use
--trace-warningsflag in development to identify leak sources