performance2025-05-28·9·50/348

Node.js EventEmitter memory leak 경고 해결

Diagnosing and fixing EventEmitter memory leak warnings in Node.js applications that crash production servers.

Node.js EventEmitter memory leak 경고 해결

Introduction

The warning (node:12345) MaxListenersExceededWarning: Possible EventEmitter memory leak detected is a signal that your application is registering too many listeners on a single event. This is one of the most dangerous warnings in Node.js because it often precedes an actual memory leak that can crash your server.

Environment

node --version
# v20.11.0

# Testing on a server with 2GB RAM
free -h
#               total        used        free      shared  buff/cache   available
# Mem:          2.0Gi       1.5Gi       128Mi        64Mi       384Mi       312Mi

Problem

In a long-running Express API server, the following warning appeared after approximately 4 hours of uptime:

(node:4521) MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
11 listeners added to [EventEmitter]. Use emitter.setMaxListeners() to increase limit
    at _addListener (node:events:510:17)
    at Socket.on (node:net:722:26)
    at new ClientRequest (node:_http_client:202:12)
    at overwriteProtocol (node:https:323:19)
    at Agent.createConnection (node:https:291:18)

The warning kept repeating with increasing listener counts:

MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 15 listeners
MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 23 listeners
MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 31 listeners

Analysis

The root cause was a logging middleware that was attaching event listeners without cleaning them up:

// BAD: Leaking listeners
function loggingMiddleware(req, res, next) {
  req.on('data', (chunk) => {
    logger.debug(`Request data: ${chunk}`);
  });
  req.on('end', () => {
    logger.info(`Request complete: ${req.method} ${req.url}`);
  });
  next();
}

The problem is that req and res objects in Express are Node.js streams, which are EventEmitters. If the middleware is called for every request and attaches new listeners each time, the listeners accumulate if they are not properly cleaned up.

To verify the leak, I used this diagnostic script:

// check-listeners.js
const EventEmitter = require('events');

const emitter = new EventEmitter();
console.log('Default max listeners:', emitter.getMaxListeners());

// Simulate the leak
for (let i = 0; i < 15; i++) {
  emitter.on('data', () => {});
}

console.log('Current listener count:', emitter.listenerCount('data'));
node check-listeners.js
# Default max listeners: 10
# Current listener count: 15

Solution

Solution 1: Remove listeners when done

// GOOD: Cleaning up listeners
function loggingMiddleware(req, res, next) {
  const onData = (chunk) => {
    logger.debug(`Request data: ${chunk}`);
  };
  
  const onEnd = () => {
    logger.info(`Request complete: ${req.method} ${req.url}`);
    // Clean up listeners
    req.removeListener('data', onData);
    req.removeListener('end', onEnd);
  };
  
  req.on('data', onData);
  req.on('end', onEnd);
  next();
}

Solution 2: Use once instead of on

function loggingMiddleware(req, res, next) {
  req.once('end', () => {
    logger.info(`Request complete: ${req.method} ${req.url}`);
  });
  next();
}

Solution 3: Increase limit intentionally (if listeners are expected)

const server = require('http').createServer(app);
server.setMaxListeners(50); // Increase from default 10

Solution 4: Use a proper monitoring solution

// monitor.js
const EventEmitter = require('events');

function checkLeaks(emitter, name) {
  const originalEmit = emitter.emit.bind(emitter);
  const listenerCounts = new Map();
  
  emitter.emit = function(event, ...args) {
    const count = emitter.listenerCount(event);
    listenerCounts.set(event, count);
    
    if (count > 10) {
      console.warn(`[${name}] High listener count on '${event}': ${count}`);
    }
    
    return originalEmit(event, ...args);
  };
}

Solution 5: Use process event listeners safely

// BAD: Adding process listeners in a loop
app.get('/download', (req, res) => {
  process.on('uncaughtException', handler); // Leaks!
  // ... handle download
});

// GOOD: Add process listeners once at startup
process.on('uncaughtException', handler);

Lessons Learned

  1. Always use removeListener or removeAllListeners when you add listeners dynamically
  2. Prefer once over on for events that should only fire once per request
  3. Monitor listener counts in production using process metrics
  4. Set NODE_ENV=production to suppress some warnings, but do not ignore them
  5. Use weak references or WeakRef where appropriate for long-lived event handlers

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.