How It Works Features Pricing Blog Error Guides
Log In Start Free Trial
Node.js · JavaScript

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

MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 'data' listeners added to [ReadStream]. Use emitter.setMaxListeners() to increase limit at _addListener (node:events:587:17) at ReadStream.addListener (node:events:605:10) at ReadStream.on (node:events:605:10) at subscribe (src/services/dataStream.js:22:14) at handleConnection (src/handlers/websocket.js:18:5) at WebSocket.emit (node:events:513:28) at WebSocket.onopen (node_modules/ws/lib/event-target.js:196:16) at WebSocket.emit (node:events:513:28) at ClientRequest.<anonymous> (node_modules/ws/lib/websocket.js:227:14) at Socket.emit (node:events:513:28)

Here's what each line means:

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.

Before (broken)
function handleConnection(ws) {
  dataStream.on('data', (msg) => {
    ws.send(JSON.stringify(msg));
  });
}
After (fixed)
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:

  1. Notice the error alert or see it in your monitoring tool
  2. Open the error dashboard and read the stack trace
  3. Identify the file and line number from the stack trace
  4. Open your IDE and navigate to the file
  5. Read the surrounding code to understand context
  6. Reproduce the error locally
  7. Identify the root cause
  8. Write the fix
  9. Run the test suite locally
  10. Fix any failing tests
  11. Write new tests covering the edge case
  12. Run the full test suite again
  13. Create a new git branch
  14. Commit and push your changes
  15. Open a pull request
  16. Wait for code review
  17. Merge and deploy to production
  18. 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:

  1. Captures the stack trace and request context
  2. Pulls the relevant source files from your GitHub repo
  3. Analyzes the error and understands the code context
  4. Generates a minimal, verified fix
  5. Runs your existing test suite
  6. Pushes through your CI/CD pipeline
  7. 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)

  1. Audit all event listener registrations for matching removeListener calls.
  2. Add cleanup handlers for connection close and error events.
  3. Run tests to confirm listeners are properly removed.
  4. Open a pull request and wait for CI.
  5. 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.