Course contentsShow
← Node.js
Lesson 1179 of 1,98010. Real-time Communication and WebsocketsFree lesson

Managing Resource Leaks in Event Listeners

You will implement robust cleanup logic to prevent memory growth caused by abandoned event listeners.

In this lesson, you will learn how to prevent memory bloat by systematically cleaning up event listeners during the lifecycle of WebSocket connections.

In Node.js, an event listener creates a strong reference between the emitter and the callback. When working with persistent connections like WebSockets, it is common to attach listeners to global objects (like a Redis client or a MessageBus) to react to specific events. If a WebSocket client disconnects but you fail to remove the corresponding listener from the global emitter, that function—and the entire closure scope it carries—remains in memory forever. This is a classic "memory leak" that will eventually crash your server as the heap grows with abandoned objects.

The "Dangling Listener" Trap

Imagine your server subscribes a socket to a Redis channel:

socket.on('connection', (ws) => {
  const handler = (data) => ws.send(data);
  
  // Potential leak: if the socket closes, 'handler' stays in redis.on
  redis.on('message', handler); 

  ws.on('close', () => {
    // If we forget this line, the closure 'handler' is never garbage collected
    redis.removeListener('message', handler);
  });
});

The handler holds a reference to the ws object. Even if the socket is closed, the redis instance keeps a reference to handler, which keeps a reference to ws, preventing the garbage collector from reclaiming the memory.

Robust Cleanup Patterns

To avoid this, treat every on() call as a contract that requires an eventual removeListener() or off(). When dealing with complex logic, encapsulate the lifecycle in a small wrapper or use the once() method if the event is truly ephemeral. If you must use on(), always tie the removal to the socket’s close or error events.

For more complex scenarios, consider using an AbortController. You can pass an AbortSignal to newer Node.js event listeners, allowing you to clean up multiple listeners in a single command (signal.abort()) rather than tracking every individual function reference.

Key Takeaway: Every event listener attached to a long-lived object during a request or socket lifecycle must be explicitly removed when that lifecycle ends to prevent permanent memory growth.