Question presented to candidate: "Your team's monitoring shows a Node process's memory growing steadily over days, never dropping, even during low-traffic periods. Where would you actually look first, and what specific number would you check?"
What a strong answer should cover:
- A memory leak in a garbage-collected language like JavaScript is not "the GC failing" — V8's garbage collector correctly reclaims memory with no remaining reachable references. A leak is memory the application itself is still holding a live reference to, unintentionally, that will never be released as a result — a growing array, cache, or closure nobody ever clears.
- 📌 A precise, verifiable distinction:
process.memoryUsage()'sheapUsedtracks the JS object heap specifically.Buffers andArrayBuffers are allocated outside that heap, tracked instead underexternal/arrayBuffers— verified directly: retaining 50MB ofBuffers barely movedheapUsed(4.0MB → 4.9MB) whileexternal/arrayBuffersgrew by over 30x (to 54.3MB/52.6MB). MonitoringheapUsedalone would completely miss this real, concrete leak. - Common real-world leak sources: an ever-growing array or Map used as a cache with no eviction policy; an event listener registered repeatedly without ever being removed (each new listener retains its own closure); a closure capturing a large object unintentionally, kept alive by something still referencing that closure; a timer (
setInterval) that is never cleared, itself retaining whatever its callback closes over. - Detection tools, from lightest to heaviest: watching
process.memoryUsage()over time (cheap, coarse, and — verified above — must check the right field, not justheapUsed); a heap snapshot comparison (covered in its own dedicated question) taken at two points in time, diffed to see what object types grew; a dedicated profiler (Chrome DevTools' memory tab attached via--inspect, orclinic.js/similar tools) for a detailed retainer-path analysis pinpointing exactly what is holding a reference. - A precise answer distinguishes a genuine leak (memory that will never be released, growing without bound over the process's lifetime) from ordinary, expected memory usage growth under load (more concurrent requests legitimately using more memory, which should stabilize or shrink again once load drops) — the "never drops, even at low traffic" detail in the prompt is exactly what marks it as the former.
- Forcing a garbage-collection pass (
--expose-gc, callingglobal.gc()) is a real diagnostic technique for confirming whether memory is genuinely leaked (unreleased even after a forced full GC pass) versus simply not yet collected — though a precise answer notes real GC behavior can still leave some memory only partially reclaimed after one pass, not a perfectly clean before/after split.
Clarifying questions expected:
- "Is memory growing under sustained load and then stabilizing/dropping, or growing indefinitely regardless of traffic?" — only the latter is a genuine leak.
- "Have Buffers/external memory specifically been ruled out, or has only
heapUsedbeen checked?" — verified above as a real, easy-to-miss distinction.
Code / implementation expected: Yes — the real, measured heapUsed vs. external/arrayBuffers split for a Buffer-based leak is the concrete, convincing deliverable, not a description of "memory can leak."