Chasing Down a Slow Memory Leak in Node or Go Before It Pages You
To find a slow memory leak in Node or Go, capture memory state at two points in time under real or realistic load and compare what grew between them. A leak rarely shows up in code review; it appears days or weeks later as a service that slows down and then restarts on its own.
The tools differ between Node and Go, but the underlying method is the same: capture memory state at two points in time under real or realistic load, and look specifically at what grew between them rather than what's simply present.
Confirming it's actually a leak before you start profiling
Rising memory usage on its own isn't proof of a leak; both Node's garbage collector and Go's runtime naturally hold onto some memory for efficiency, and usage that climbs then plateaus at a stable level is normal, healthy behavior. A real leak shows a memory graph that keeps climbing without ever plateauing, over a timeframe long enough to rule out normal garbage collection cycles or cache warm up.
Watch the growth pattern against traffic, not just against time. Memory that grows in proportion to request volume and then comes back down when traffic drops is a sign of a working cache or connection pool, not a leak; memory that keeps climbing even during a quiet period with steady low traffic is the pattern actually worth investigating.
Signs that rising memory is a real leak:
- Memory keeps climbing without ever plateauing, over a timeframe long enough to rule out normal garbage collection cycles or cache warm up.
- Usage keeps climbing even during a quiet period with steady, low traffic.
- Growth that tracks request volume and comes back down when traffic drops points to a working cache or connection pool, not a leak.
- Usage that climbs and then plateaus at a stable level is normal, healthy behavior, so rule that pattern out first.
How do you find a Node leak with heap snapshots?
Capture a heap snapshot early after the process starts, let the suspected leak accumulate under realistic load for a meaningful stretch of time, then capture a second snapshot and compare the two directly rather than examining either one in isolation. The comparison view highlights what grew between the two snapshots, which narrows the search dramatically compared to reading a single snapshot's full object graph.
Pay close attention to objects retained by a closure or an event listener that was never removed, since an event emitter that accumulates listeners over time without ever unsubscribing them is one of the most common real leak patterns in a long running Node service, and it shows up clearly as a steadily growing count of listener objects in the comparison view.
How do you find a Go leak with pprof?
Go's pprof tooling profiles heap allocations directly, and the same two point comparison approach applies: capture a profile early, let the process run under load, capture a second profile, and diff them to see what's actually accumulating rather than what's simply allocated. The diff view sorts by the objects contributing the most growth, which is usually where the actual leak lives.
A goroutine leak, goroutines started but never exiting, is a distinct and common Go specific failure mode worth checking separately from heap growth, since a goroutine that blocks forever on a channel that's never written to holds its stack memory indefinitely. pprof's goroutine profile, not just its heap profile, is the tool for catching this specific pattern, and a steadily climbing goroutine count over time is as strong a signal as climbing heap memory.
Fixing it without introducing a second leak
Once you've found the accumulating object, the fix is usually straightforward, unsubscribing a listener, closing a channel, clearing a cache with unbounded growth, but verify the fix with the same two snapshot comparison method rather than assuming the code change worked. It's a genuinely common mistake to fix the specific leak that was found while introducing a slightly different one in the same area of code during the fix itself.
Add the leak scenario to your regular test suite if it's practical to reproduce, running the code path in a loop and asserting memory stays within a reasonable bound. This catches a regression the next time someone touches that code, rather than waiting for the leak to reappear in production months later and having to rediscover the whole diagnosis from scratch.
What Good Looks Like
Good leak diagnosis confirms a real leak against traffic patterns before profiling, uses a two point snapshot comparison to narrow the search instead of reading a single profile blind, and verifies the fix with the same method rather than assuming it worked.
Building The Capability (5-Stage Skill Ladder)
How to Get Started
Frequently Asked Questions
How do we know if rising memory usage is a real leak or just normal garbage collector behavior?
Watch the growth pattern over a meaningful stretch of time and against traffic. Memory that plateaus at a stable level, or that rises and falls with traffic volume, is normal. Memory that keeps climbing steadily even during a quiet, low traffic period without ever plateauing is the pattern that actually indicates a leak worth investigating.
What's the most common cause of a memory leak in a long running Node service?
An event listener or callback that's registered but never removed, often on an event emitter that outlives the code that registered the listener. This shows up clearly in a heap snapshot comparison as a steadily growing count of listener objects between two snapshots taken under sustained load.
Is a goroutine leak in Go different from a regular heap memory leak?
Yes, and it's worth checking separately. A goroutine leak happens when a goroutine blocks forever, often waiting on a channel that's never written to, holding its stack memory indefinitely without ever exiting. Use pprof's goroutine profile specifically, not just its heap profile, to catch a steadily climbing goroutine count.
About the numbers
This guide doesn't quote a sourced benchmark. Figures in it are estimates or general guidance, so check them against your own numbers.
Related Guides
Finding a Memory Leak Before It Pages You
A service's memory climbs for days until it gets killed and restarts, then climbs again. How to profile Node and Go to trace a leak to its real cause.
Finding a Memory Leak Before It Finds Your Pager
A worked walkthrough of diagnosing a memory leak: heap snapshots in Node.js, pprof in Go, and capturing evidence before the process gets killed.
Finding a Memory Leak in Node or Go Before It Takes Down a Pod
How to use heap snapshots and pprof to find a real memory leak in Node.js or Go, and the common causes behind a slow, steady memory climb in production.
Tracking Down a Slow Memory Leak in Node or Go, Step by Step
A worked walkthrough of profiling and fixing a slow memory leak in a Node.js or Go service, from spotting the pattern to confirming the fix actually worked.
Profiling a Memory Leak in Node or Go Before It Pages You
A practical approach to finding a memory leak in Node.js or Go, including the tools to reach for first and the leak patterns specific to each.
Chasing Down a Memory Leak in Node and Go Services
How to tell a real leak from normal garbage collection, take a useful heap snapshot, and stop shipping scheduled restarts as the fix.