The second talk at the Go Amsterdam meetup at JetBrains was Mohammed Nafees’s “The Curious Case of the Missing Memory” — a forensic look at why a Go process reports far more memory in use than your heap profile suggests. If you have ever stared at a pod OOMKilled while pprof swore the heap was tiny, this one was for you.
The Premise
Go’s garbage collector is excellent at reclaiming unreachable objects, but “memory my program uses” is not the same as “memory the GC tracks.” The gap between the two is where the mystery lives. Mohammed walked through a real investigation: the process RSS kept climbing, the heap profile looked innocent, and the usual suspect (a leak in your structs) was a red herring.
Where Memory Actually Goes
A few sources of “missing” memory that don’t show up where you first look:
- The garbage collector’s own overhead. Go keeps a target heap size and lets the live heap grow to it before collecting aggressively. The runtime also retains memory it has freed back to the OS lazily, so RSS can stay high even after objects are collected.
- Retention you didn’t intend. A single long-lived slice, map, or channel holding a reference transitively keeps everything behind it alive. The classic case: appending to a slice that occasionally grows, leaving a huge backing array allocated even when most of it is unused.
- Cgo and off-heap allocations. Memory allocated outside Go’s managed heap (C libraries, certain syscalls) is invisible to the Go GC and to
pprof’s heap view. - Stacks and goroutines. Each goroutine starts with a small stack that grows; a fleet of blocked goroutines adds up.
- Fragmentation and large allocations. Objects over a certain size take a different allocation path, and fragmentation can leave memory unused but not returned.
The Investigation Toolkit
Mohammed’s method was the part worth stealing:
- Start from
/debug/pprof/heapandallocs— but read them alongsideruntime.MemStatsand thego tool pprofinuse_spacevsinuse_objectsviews. - Compare RSS (what the OS sees) against the heap profile (what Go manages). A large gap points to GC overhead, retained-but-idle memory, or off-heap allocations.
- Use
GODEBUG=gctrace=1to watch collection frequency and freed bytes. - For goroutine leaks,
pprof/goroutineplus a quickruntime.NumGoroutine()trend tells the story fast.
The punchline: “missing memory” is rarely magic. It is usually retention, GC accounting, or something off the Go heap entirely — and the toolchain already gives you the evidence if you look at RSS and the heap together.
Takeaway
Memory bugs in Go are less about the language and more about understanding the boundary between the runtime’s view and the operating system’s view. Mohammed’s talk was a clear, practical case study in reading that boundary instead of guessing.
Related Content
- The Go Programming Language: a practical guide
- Go Meetup Amsterdam: A Deep Dive into the select Statement
- Go Meetup Amsterdam: Staying Passionate in Tech in the Age of AI
- Go Meetup Amsterdam: Event Recap











