In this guide 6 sections
A Node.js service that climbs in memory until it restarts usually has a leak in your code, not a faulty server. Common sources are unbounded caches, event listeners added on every request, unreleased database connections and large report exports. Heap snapshots show which objects keep growing.
Confirm it is a leak and not normal growth
Node.js lets memory climb before the garbage collector runs, so a rising line alone proves little. Watch heapUsed from process.memoryUsage() over a few hours. A leak shows a sawtooth whose floor keeps rising after each collection.
Note when the restart happens. A process killed by the operating system or by a container limit reports an out of memory exit. A crash with a JavaScript heap error means the V8 limit was reached first. The two need different handling.
Take heap snapshots and compare them
Start the service with the --inspect flag in staging, connect Chrome DevTools and take a heap snapshot. Run realistic traffic for twenty minutes, then take a second one. Use the comparison view to list objects that grew between the two.
Look at the retainers, the chain of references keeping each object alive. Production can write a snapshot with v8.writeHeapSnapshot() on a signal, but the file is large and pauses the process, so schedule it outside peak hours.
- Snapshot one at a clean start
- Snapshot two after steady load
- Sort the comparison view by size delta
- Read the retainer path of the biggest growers
The usual leak sources in business apps
The first is a cache built from a plain object or Map, keyed by customer or order, with no size limit and no expiry. Every new key stays forever. Use an LRU cache with a maximum entry count and a time to live.
The second is listeners. Calling emitter.on() inside a request handler adds a new listener per request, and Node eventually prints a MaxListenersExceededWarning. Treat that warning as a bug report. Timers created with setInterval and never cleared leak in the same way.
Connections and large exports
A database client created per request holds buffers and sockets. So does a pooled connection never released on an error path. Use one shared pool, and release the client in a finally block. Check the pool counters in PostgreSQL or MySQL during load.
Exports are the other big one. Reading fifty thousand invoice rows into an array before writing the Excel file puts the whole set in memory at once. Stream rows from a cursor into the file instead, and process in batches.
Contain the damage while you fix it
Run the service under PM2, systemd or an orchestrator that restarts it cleanly, and set --max-old-space-size below the container limit so Node fails with a readable error. Alert on memory trend, not only on a crash.
Fix one source at a time and rerun the same load test. Keep the before and after charts. If memory flattens, you found it. If not, take a new snapshot and look at the next retainer.
Node.js memory leak triage sheet
A one page worksheet. Rows list each suspect: caches, listeners, timers, connection pools, exports and file handles. Columns record where it lives in the code, the evidence from snapshots, the fix applied and the memory chart before and after. A header box holds the Node version, container limit and flags.
Open a blank worksheet to printQuestions owners ask
How do I tell a leak from a busy server?
Hold the traffic steady and watch memory after garbage collection. If the low points keep rising over hours, objects are being retained. If they stay level, the process is just busy.
Is restarting every night an acceptable fix?
It is a stopgap. It hides the leak, and a busy day can still exhaust memory before the schedule runs. Use it only while you track down the cause.
Can I run heap snapshots in production?
You can, but taking one pauses the process and needs memory about equal to the heap. Take it on one instance outside peak hours, or reproduce the growth in staging.
Do third party packages cause leaks?
Sometimes. If retainer paths point into node_modules, check the package’s issue tracker and upgrade, then confirm with a fresh snapshot.
