Build1 publisher2 min readPublished
V8 promotes the object you forgot to release into the space it collects less often
A dev.to guide on Node.js memory leaks separates resident set size from the V8 heap and names four retention patterns that keep objects reachable long enough to be promoted into old space. Each has a code-level fix.
The Engineer · Build desk

What happened
- Four production leak causes are named: undeclared globals, event listeners on long-lived emitters, closures holding parent scope, and in-memory caches with no TTL or size cap.
- The prescribed fixes are code-level: strict mode or ESLint for undeclared variables, removing the listener when the response finishes, and lru-cache, Redis or Memcached instead of a plain object.
- The failure the guide targets shows up first as latency and degradation, then as V8 aborting with the allocation-failed fatal error naming the JavaScript heap.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint A dashboard that plots only process memory cannot separate heap retention from growth in native buffers or code, so the graph most teams already have on call does not narrow the search.
- decision Attaching a listener to a shared emitter inside a request handler makes teardown part of that handler's contract: some line has to own the off() call, and the response's finish event is where the guide puts it.
- exposure Retention that scales with requests served means the busiest instance in a fleet hits the allocation failure first, and the quiet one looks healthy while carrying the same bug.
- capability All four patterns are shapes in source code, so a suspected leak can be narrowed during review before anyone captures a heap snapshot on a production box.
V8 raises that fatal error when it cannot grow the JavaScript heap [3]. The heap is one part of what a host reports as the process's memory. Resident set size covers the C++ Node bindings, the code segment, the stack and the heap together [5]. An RSS curve that climbs does not tell you whether the growth sits in the region the error names [4].
The promotion rule is what makes a leak durable. New allocations go into New Space, where the Scavenge collector runs often and cheaply [6]. Anything still reachable after several of those cycles moves to Old Space, which V8 collects less frequently using Mark-Sweep-Compact [7]. A leak is a reference you did not drop [2], so the object survives every scavenge and lands in the slower-collected space. The guide says leaks almost always manifest in the Old Space heap [8].
Of the four patterns, the listener case is the one I would check first in a request handler. Registering `globalEmitter.on('update', handler)` inside the handler closes over `res`, and the guide's comment on that line says the `res` object is retained in memory [11]. The fix is one statement in the same function: `res.on('finish', () => globalEmitter.off('update', onUpdate))` [12]. Without it, each request served leaves one more listener and one more response object reachable [3].
The closure example is the one with numbers in it. `new Array(1000000).join('*')` puts a separator between each pair of elements, so the string is 999,999 characters long [1]. `setInterval(replaceThing, 1000)` runs it every second [14], which is 3,600 of those strings an hour [2]. The unused inner function keeps the previous object reachable through `originalThing`, and the guide annotates the interval line "Heap growth accelerates continuously!" [14].
Two of the remaining fixes are cheap to adopt. Strict mode at the top of the file, or ESLint, catches the undeclared assignment that becomes global and then lives for the whole process lifecycle [9][10]. A plain object used as a cache with no TTL and no size cap keeps growing, and the guide's alternatives are `lru-cache`, Redis or Memcached [15][16].
The guide's introduction says it will cover step-by-step diagnostic techniques using built-in tools, and the published text breaks off inside the `process.memoryUsage()` logging example [18]. A nightly restart also fixes all of this, in the sense that a nightly restart fixes anything that takes more than a day to grow. What the accounting gives you is cheaper than a profiler: `process.memoryUsage()` reports the split from inside the process [17], and the question on the instance that crashes is whether old space keeps climbing after the request rate falls.
What to watch
- Publication of the guide's diagnostic section, which would let the code-reading path be checked against a measured heap snapshot diff.
- Any measurement of which of the four named patterns actually dominates production crashes, which would test the claim that leaks almost always sit in Old Space.
- Whether teams instrument the RSS-versus-heap split from process.memoryUsage() rather than a single memory metric per pod.