Fix Node.js MaxListenersExceededWarning
Node's MaxListenersExceededWarning means a listener is piling up somewhere. Here's how to find and fix the real leak instead of raising the limit.
(MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 listeners added. Use emitter.setMaxListeners() to increase limit.) That's the full text of the warning, and it tells you almost nothing useful. Your app keeps running, the logs fill up with this exact line over and over, and nobody on the team knows which listener is actually the problem.
Node's MaxListenersExceededWarning fires the moment a single EventEmitter picks up more than 10 listeners for the same event — 10 is the default cap, not a hard limit, just a tripwire. Most of the time that tripwire is doing its job: something in your code is attaching a new listener on every request, every connection, or every loop iteration, and never removing it. Left alone, that's a real memory leak, and on a long-running Node process it will eventually show up as climbing RSS and, if you're unlucky, a crash under load.
What MaxListenersExceededWarning Actually Means
Every EventEmitter instance — http.Server, a database client, a custom pub/sub bus, a Readable stream — tracks its own listener count per event name. Call .on() or .addListener() enough times on the same instance for the same event, and once you cross 10, Node prints the warning once per emitter instance.
The number in the message (11 listeners added) is the count at the moment it tripped, not necessarily where it stops. I've seen this warning get ignored for months on a client's app because "it still works," while the listener count quietly climbed into the hundreds.
Finding Which Listener Is Piling Up
Before touching any code, confirm where the listeners are actually accumulating. Node gives you two built-ins for this — emitter.listenerCount(eventName) and, since Node 18, events.getEventListeners(emitter, eventName):
Point that at whatever emitter you suspect — a database pool, a long-lived socket, your own app-level event bus — and watch the number over a few minutes of normal traffic. If it climbs instead of holding steady, you've confirmed a leak rather than a one-time burst, which matters because the fix for each is different.
The Lazy Fix (And Why I'd Skip It)
emitter.setMaxListeners(20) — or 0 to disable the check entirely — makes the warning go away in about ten seconds. I've done it myself, late on a Friday, just to stop the log noise before a demo.
Here's my actual take on it: raising the cap is almost never the right move unless you already understand why the count is high and you've decided that number is structurally correct — a connection pool with 30 workers each registering a shared listener, say. Outside that narrow case, setMaxListeners doesn't fix anything, it just moves the ceiling up so the real leak has more room to grow before anything crashes. If you reach for it as a first response rather than a last one, you're choosing to find out about the leak in production instead of in your logs.
The Real Fix: Remove Listeners When You're Done With Them
Almost every case I've actually debugged comes down to one of two things: a listener attached inside a function that runs repeatedly, or a listener that outlives the thing it was listening to. The fix is the same either way — stop treating .on() as fire-and-forget.
// Leaking: a new listener on every call, never removedfunction watchJob(queue, jobId) {queue.on('completed', (job) => {if (job.id === jobId) console.log('done:', jobId);});}// Fixed: one-time listener that removes itselffunction watchJob(queue, jobId) {const handler = (job) => {if (job.id !== jobId) return;console.log('done:', jobId);queue.off('completed', handler);};queue.on('completed', handler);}
emitter.once() solves the same problem when you genuinely only care about the event firing a single time — it removes itself automatically after the first call, so there's nothing left to clean up. For anything longer-lived, keep a reference to the handler so you can call .off() (or the older .removeListener()) exactly once, in whatever teardown path applies — a close handler, a finally block, a component unmount, a request's res.on('finish', ...).
Where This Usually Comes From: Per-Request Listeners
The specific shape I run into most often on client projects is a listener attached inside a request handler. Something like a Postgres pool or a shared queue client gets a .on('error', ...) or .on('notification', ...) added fresh on every incoming HTTP request, and nothing ever removes it, because request handlers don't normally think of themselves as needing teardown logic.
I hit this exact pattern on an Express API a few months back: every request called a helper that did pool.on('query', logSlowQuery), meant to log that one query. After a few thousand requests the pool had a few thousand listeners sitting on it, RSS was climbing steadily, and the warning had been in the logs for weeks before anyone looked at it. The fix wasn't clever — move the listener registration to app startup, once, and have it inspect the query that's passed to it instead of being re-created per request.
If you're attaching a listener inside anything that runs more than once — a route handler, a job processor, a loop — ask whether that listener actually needs to be scoped to this one call, or whether it belongs at the top level, registered exactly once for the life of the process. That per-call registration pattern is the single most common root cause of MaxListenersExceededWarning I've seen across client codebases, more than any exotic edge case.
Frequently Asked Questions
Does setMaxListeners(Infinity) cause any real problem beyond hiding the warning?
Not by itself — it's just a number. The actual cost is CPU and memory spent invoking and holding references to listeners that have no reason to still exist, which is the leak itself, not the setting. Infinity just means Node will never warn you while that cost keeps growing.
Can this warning come from a library I don't control, not my own code?
Yes, and it's worth checking the stack trace Node attaches to the warning (--trace-warnings flag) before assuming it's your bug. If a dependency is the one calling .on() repeatedly on a shared emitter, that's a bug report for that library, not something setMaxListeners on your side should paper over long-term.
Conclusion
The warning is doing exactly what it's supposed to: catching a growing listener count before it becomes a production incident. Treat the number in the message as a symptom, confirm it's actually climbing with listenerCount before you touch anything, and fix the registration site — not the cap — once you find it. If you've already raised the limit somewhere to make the noise stop, that's the first place to go back and check.