Programming

Node.js Express app showing unbounded memory growth traced to accumulating event listeners and timers

Environment

Environment

Node.js 18 application running Express behind PM2 on Linux, single-process mode, serving REST APIs; observed memory growth over days under typical traffic causing occasional OOM kills from system OOM killer; third-party libraries include an MQTT client and a database driver.

Symptoms

Symptoms

Resident set size (RSS) and heapUsed grow steadily over time; response latency increases when memory is high; processes crash without uncaughtException logs when OOM occurs; heap snapshots show many instances of closures and EventEmitter listeners tied to routes and long-lived connections.

Observations

Observations

Heap snapshots taken at 6-hour intervals show retained objects increasing in the same code paths; process._getActiveHandles reveals many timers created from setInterval that are not cleared; running --trace-warnings shows 'MaxListenersExceededWarning' for an EventEmitter in a third-party module that is created per request; removing a specific middleware reduces growth rate but does not fully stop it.

Already attempted

Already attempted

Upgraded Node and dependencies to latest minor versions, added global logging for new connections and disconnections, instrumented GC with --trace-gc, took multiple heap snapshots and compared retained object paths using Chrome DevTools, added process.env.NODE_OPTIONS='--max-old-space-size' as temporary mitigation and configured PM2 to restart on memory threshold.

Question

Question

What targeted diagnostic steps will reliably identify the code paths retaining listeners and timers, and what reversible code changes can I deploy to stop the leak without removing entire middleware features?

COMMUNITY RESPONSES

Verified community troubleshooting

0 replies

No answers yet..

Showing 0 of 0 community responses
AI DIAGNOSTICS

Incident analysis

Updated 1w ago• Trouble ID

Most likely causes

Uncleared setInterval/setTimeout timers created per request or per connection 95%
Attaching process- or module-scoped event listeners inside per-request code paths without removing them 90%
Third-party module that instantiates a global emitter per use and doesn't expose a cleanup API 85%

Summary

The pattern of accumulating EventEmitter listeners and uncleared timers indicates a resource lifecycle bug: either per-request objects are registering process-level listeners and never deregistering, or third-party modules create listeners/timers on every instantiation without cleanup. setInterval/setTimeout and EventEmitter leaks commonly arise from attaching listeners in request handlers or failing to call removeListener/clearInterval on error or connection close paths. The MaxListenersExceededWarning points to an emitter that is being reused but not cleaned up.

Suggested Fixes

Diagnostic step 1
Produce time-series heap snapshots and use heapdiff to identify retained object types and their retaining paths; automate snapshot capture under normal load and after a restart.
Diagnostic step 2
Search codebase for EventEmitter.on/addListener and setInterval/setTimeout calls inside request handlers or modules initialized per-request; refactor to attach listeners once at process start or ensure removal on request end (use emitter.removeListener or emitter.off and clearInterval).
Diagnostic step 3
Wrap third-party clients to a single shared instance when possible, and if a library creates listeners per usage, contact maintainer or add cleanup by calling provided close/destroy functions on request completion.

Continue troubleshooting

Matched from the title, description and tags