Your Node.js process starts at 150 MB, and a few days later it’s at 1.5 GB, until it crashes with “JavaScript heap out of memory”, or the Linux OOM killer ends it. That’s a memory leak: objects that are no longer needed, but are still referenced, so the garbage collector can’t free them. Here’s how to confirm a leak, find it, and keep production stable meanwhile.
Heap vs RSS
setInterval(() => {
const m = process.memoryUsage();
console.log(JSON.stringify({
rss_mb: (m.rss / 1e6).toFixed(0),
heap_used_mb: (m.heapUsed / 1e6).toFixed(0),
heap_total_mb: (m.heapTotal / 1e6).toFixed(0),
external_mb: (m.external / 1e6).toFixed(0),
}));
}, 60000);
- heapUsed growing steadily, and not dropping after garbage collection → a JavaScript object leak.
- RSS growing while the heap is stable → native memory (Buffers, native addons, many concurrent requests), or fragmentation.
Memory that rises under load and falls back when traffic drops isn’t a leak. Watch the trend over hours or days. PM2 (pm2 monit) or Prometheus metrics (prom-client default metrics) make this easy. See observability basics.
Set a sensible heap limit
node --max-old-space-size=512 server.js # MB
Set it below the container or VPS memory limit, so Node crashes with a clear error (and restarts) instead of the whole server swapping or the OOM killer picking a random process. See Linux OOM killer and Docker CPU and memory limits.
Take heap snapshots
A heap snapshot records every object in memory. Compare two snapshots taken some time apart, to see what grew.
Trigger snapshots on demand in production (they pause the process for seconds, and need roughly as much free memory as the heap):
const v8 = require("node:v8");
process.on("SIGUSR2", () => {
const file = v8.writeHeapSnapshot(`/tmp/heap-${Date.now()}.heapsnapshot`);
console.log("heap snapshot written to", file);
});
kill -USR2 <pid> # snapshot 1, after warm-up
# … wait while memory grows (e.g. 30 minutes of traffic) …
kill -USR2 <pid> # snapshot 2
Or start Node with --heapsnapshot-signal=SIGUSR2. Then copy the files to your computer (scp), and open them in Chrome DevTools → Memory → Load. Use the Comparison view: sort by “# Delta” or “Size Delta”, and look at retainers (what keeps the objects alive).
On a staging server you can also attach DevTools live, with node --inspect=127.0.0.1:9229, plus an SSH tunnel. See SSH tunnelling.
Common causes
| Leak pattern | Example |
|---|---|
| Unbounded caches | A Map or object used as a cache, without size limits or TTL |
| Event listeners | emitter.on(...) added per request, and never removed (watch for MaxListenersExceededWarning) |
| Global arrays | Logs, metrics or requests pushed into arrays that only grow |
| Closures | Callbacks holding references to large objects (requests, responses) |
| Timers | setInterval per user/connection, never cleared |
| Unclosed resources | Database connections, streams, sockets left open |
| Libraries | Known leaks in specific versions. Check issues and update |
Fixes: use LRU caches with a maximum size (lru-cache), remove listeners (once, off), clear timers, close resources in finally blocks, and keep request-scoped data out of module-level variables.
Stopgaps while you investigate
- Restart on memory threshold: PM2’s
max_memory_restart: "500M"restarts an instance before it gets dangerous. With cluster mode and graceful shutdown, users don’t notice. See Node.js cluster mode. - Scheduled restarts during quiet hours.
- These hide the problem, so keep the investigation going.
Prevent regressions
- Load-test before releases, and watch memory over a long run. See load testing with k6.
- Alert when heap usage keeps rising for hours. See alerting best practices.
- Keep Node.js and dependencies updated. See install Node.js.
Quick reference
| Task | How |
|---|---|
| Watch memory | process.memoryUsage(), pm2 monit |
| Heap limit | --max-old-space-size=<MB> |
| Snapshot | v8.writeHeapSnapshot() / --heapsnapshot-signal |
| Compare | Chrome DevTools → Memory → Comparison |
| Stopgap | PM2 max_memory_restart |
| Usual culprits | Caches, listeners, timers, globals |
Related: PM2 process manager and Linux memory usage high.