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 312MiProblem
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 listenersAnalysis
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: 15Solution
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 10Solution 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
- Always use
removeListenerorremoveAllListenerswhen you add listeners dynamically - Prefer
onceoveronfor events that should only fire once per request - Monitor listener counts in production using process metrics
- Set
NODE_ENV=productionto suppress some warnings, but do not ignore them - 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.