Search across all documentation pages
Every on without matching off keeps closures alive in long-running Node servers - use AbortSignal, explicit removal, and heed MaxListenersExceededWarning before heap growth becomes an incident.
import { EventEmitter } from 'node:events';
const controller = new AbortController();
const { signal } = controller;
function onAbortable(emitter: EventEmitter, event: string, fn: () => void) {
emitter.on(event, fn);
signal.addEventListener('abort', () => emitter.off(event, fn), { once: true });
}
// cleanup: controller.abort();When to reach for this:
message handlers per connectionprocess.on hooks in testsimport { EventEmitter } from 'node:events';
const globalBus = new EventEmitter();
function handleRequest(requestId: string): void {
const controller = new AbortController();
const onJob
// Detect leaks in dev
process.on('warning', (w) => {
if (w.name === 'MaxListenersExceededWarning') {
console.error('Listener leak suspected:', w.message);
}
});What this demonstrates:
AbortSignal centralizes cleanup for multiple listenersMaxListenersExceededWarning on globalBus means duplicate on without offprocess.on('warning') surfaces listener threshold breachesevents map.req, socket, or large graphs - GC cannot collect until listener removed.server.on('connection') is fine; per-connection on on global singleton is not.| Pattern | Use |
|---|---|
off with fn reference | Known handler |
AbortSignal | Request/socket scope |
once | Single fire |
removeAllListeners | Shutdown only |
import { on } from 'node:events';
// Async iterator auto-cleans when loop breaks with return
async function drain(emitter: NodeJS.EventEmitter, event: string, signal: AbortSignal) {
for await (const [payload
on - cannot off later. Fix: named function or AbortSignal wrapper.EventEmitter.removeAllListeners - flaky subsequent tests. Fix: afterEach cleanup.| Alternative | Use When | Don't Use When |
|---|---|---|
| BullMQ / queue | Cross-process events | Same-process microsecond latency |
| RxJS Subject | Complex stream operators | Simple one-to-many notify |
| Direct function call | Known single consumer | Unknown fan-out set |
events.on async iterator | Consume until done | Need emit to many unknown listeners |
More than 10 listeners for one event on one emitter by default - often duplicate registration.
Yes in Node - dangerous in production; fixes leak warnings without fixing leaks.
Log emitter.eventNames() and listenerCount('event') periodically in staging.
Only for one-shot - repeated subscriptions still need off per scope.
socket.on('message', handler) and socket.off on close - or AbortSignal per connection.
Single listener for app lifetime is OK - duplicate on hot reload is not.
Module destroy hooks should unsubscribe - verify @OnEvent handlers scope.
Yes in long-lived APIs attaching to singletons - top cause of slow memory growth.
No - per process. Leaks are per worker - multiply by worker count.
Rare pattern - explicit off is simpler and clearer in code review.
Assert listenerCount returns to baseline after simulated request end.
domain module deprecated - use AsyncLocalStorage + explicit cleanup instead.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.