Fix MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 'data' listeners added to [ReadStream]. Use emitter.setMaxListeners() to increase limit in Node.js
This warning means you are adding more event listeners than the default maximum of 10 to a single EventEmitter instance, suggesting a memory leak. Fix it by removing listeners when they are no longer needed, using once() for one-time events, or restructuring code to avoid repeatedly attaching listeners.
Reading the Stack Trace
Here's what each line means:
- at _addListener (node:events:587:17): Node's internal event system detected that more than 10 listeners are registered on this emitter, triggering the warning.
- at subscribe (src/services/dataStream.js:22:14): Each call to subscribe() on line 22 attaches a new listener without removing old ones, causing the leak.
- at handleConnection (src/handlers/websocket.js:18:5): Every new WebSocket connection triggers a subscription, so listeners accumulate as clients connect.
Common Causes
1. Listeners added on every request without cleanup
Each incoming request or connection adds a listener but never removes it, so they pile up over time.
function handleConnection(ws) {
dataStream.on('data', (msg) => {
ws.send(JSON.stringify(msg));
});
// Listener is never removed when ws disconnects
}
2. Re-subscribing in a loop or interval
Calling .on() inside setInterval or a retry loop adds duplicate listeners on each iteration.
setInterval(() => {
emitter.on('update', handleUpdate); // Adds a new listener every 5 seconds
}, 5000);
3. Using setMaxListeners to mask the problem
Increasing the max listener count hides the warning but the leak continues to grow memory usage.
emitter.setMaxListeners(100); // Masks the real issue
The Fix
Store a reference to the listener function so it can be removed later. When the WebSocket closes or errors, remove the listener from the data stream to prevent accumulation and memory leaks.
function handleConnection(ws) {
dataStream.on('data', (msg) => {
ws.send(JSON.stringify(msg));
});
}
function handleConnection(ws) {
const onData = (msg) => {
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify(msg));
}
};
dataStream.on('data', onData);
ws.on('close', () => {
dataStream.removeListener('data', onData);
});
ws.on('error', () => {
dataStream.removeListener('data', onData);
});
}
Testing the Fix
const EventEmitter = require('events');
const { handleConnection } = require('./websocket');
describe('handleConnection', () => {
let dataStream;
let mockWs;
beforeEach(() => {
dataStream = new EventEmitter();
mockWs = new EventEmitter();
mockWs.readyState = 1;
mockWs.OPEN = 1;
mockWs.send = jest.fn();
});
it('removes listener when WebSocket closes', () => {
handleConnection(mockWs);
expect(dataStream.listenerCount('data')).toBe(1);
mockWs.emit('close');
expect(dataStream.listenerCount('data')).toBe(0);
});
it('removes listener on WebSocket error', () => {
handleConnection(mockWs);
mockWs.emit('error', new Error('connection reset'));
expect(dataStream.listenerCount('data')).toBe(0);
});
it('forwards data to open WebSocket', () => {
handleConnection(mockWs);
dataStream.emit('data', { value: 42 });
expect(mockWs.send).toHaveBeenCalledWith('{"value":42}');
});
});
Run your tests:
npm test
Pushing Through CI/CD
git checkout -b fix/nodejs-event-emitter-leak,git add src/handlers/websocket.js src/handlers/__tests__/websocket.test.js,git commit -m "fix: remove event listeners on WebSocket disconnect to prevent leak",git push origin fix/nodejs-event-emitter-leak
Your CI config should look something like this:
name: CI
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test -- --coverage
- run: npm run lint
The Full Manual Process: 18 Steps
Here's every step you just went through to fix this one bug:
- Notice the error alert or see it in your monitoring tool
- Open the error dashboard and read the stack trace
- Identify the file and line number from the stack trace
- Open your IDE and navigate to the file
- Read the surrounding code to understand context
- Reproduce the error locally
- Identify the root cause
- Write the fix
- Run the test suite locally
- Fix any failing tests
- Write new tests covering the edge case
- Run the full test suite again
- Create a new git branch
- Commit and push your changes
- Open a pull request
- Wait for code review
- Merge and deploy to production
- Monitor production to confirm the error is resolved
Total time: 30-60 minutes. For one bug.
Or Let bugstack Fix It in Under 2 minutes
Every step above? bugstack does it automatically.
Step 1: Install the SDK
npm install bugstack-sdk
Step 2: Initialize
const { initBugStack } = require('bugstack-sdk')
initBugStack({ apiKey: process.env.BUGSTACK_API_KEY })
Step 3: There is no step 3.
bugstack handles everything from here:
- Captures the stack trace and request context
- Pulls the relevant source files from your GitHub repo
- Analyzes the error and understands the code context
- Generates a minimal, verified fix
- Runs your existing test suite
- Pushes through your CI/CD pipeline
- Deploys to production (or opens a PR for review)
Time from error to fix deployed: Under 2 minutes.
Human involvement: zero.
Try bugstack Free →No credit card. 5-minute setup. Cancel anytime.
Deploying the Fix (Manual Path)
- Audit all event listener registrations for matching removeListener calls.
- Add cleanup handlers for connection close and error events.
- Run tests to confirm listeners are properly removed.
- Open a pull request and wait for CI.
- Monitor listener counts in production with process.EventEmitter.listenerCount.
Frequently Asked Questions
BugStack runs the fix through your existing test suite, generates additional edge-case tests, and validates that no other modules are affected before marking it safe to deploy.
BugStack never pushes directly to production. Every fix goes through a pull request with full CI checks, so your team can review it before merging.
Only if you genuinely need many listeners and have verified each is intentional. In most cases, the warning indicates a real leak that setMaxListeners will only hide.
Use process.on('warning', ...) to catch MaxListenersExceededWarning events, and monitor emitter.listenerCount() for key emitters using your APM tool.