A memory leak is memory that a program allocates but never releases even though it no longer needs it. Usage grows steadily with time or traffic until the process slows down from garbage collection pressure, swaps, or is terminated by the operating system or container runtime with an out-of-memory error.
What is a memory leak?
A memory leak occurs when a program keeps memory it will never use again. In languages with manual memory management such as C and C++, the classic case is memory allocated with malloc or new that is never freed. In garbage-collected runtimes like the JVM, .NET, Node.js, Python or Go, the collector frees only objects that are unreachable, so a leak means that something still holds a reference to data that is logically dead. From the collector's point of view that data is alive, and it is never reclaimed.
Leaks rarely cause an immediate failure. They show up as slowly rising memory usage, longer and more frequent pauses in garbage collection, and eventually a crash or an OOM kill, often hours or days after the deployment that introduced them.
Common causes of memory leaks
- Unbounded caches and maps. A static map used as a cache without size limit or eviction grows with every new key.
- Listeners and callbacks that are never removed. Event handlers, observers or subscriptions keep their owners alive.
- Thread-local and request-scoped data that is not cleared when threads are pooled and reused.
- Closures in JavaScript that capture large objects, or timers and intervals that are never cleared.
- Goroutine or thread leaks: blocked workers that never exit, each holding its stack and referenced data.
- Native memory outside the managed heap, for example direct buffers, native libraries or unclosed file and socket handles.
- High-cardinality in-process metrics, where every new label value creates a new series kept in memory.
How to detect a memory leak
The key signal is not high memory, but memory that does not come back down. A healthy managed process shows a sawtooth pattern: usage rises with allocations and drops after each collection. With a leak, the low points of the sawtooth climb over time. Useful signals include:
- Heap used after garbage collection, for example the OpenTelemetry metric
jvm.memory.used_after_last_gcfor Java. - Resident set size of the process and, in Kubernetes, the container working set compared with its memory limit.
- GC time and frequency: as free space shrinks, the collector runs more often and reclaims less.
- Correlation with deployments or traffic: a leak that grows per request scales with load, a leak that grows per hour points to background work.
Comparing the current trend with a baseline from previous releases makes slow leaks visible long before they cause an outage.
Example: confirming and locating a leak
An illustrative case: a Java service is restarted every few days with exit code 137. The dashboard shows heap after GC rising from roughly 300 MB to 1.4 GB over 48 hours while traffic stays flat. That rules out load and points to a leak. The team then:
- Takes two heap dumps a few hours apart (
jcmd <pid> GC.heap_dump) and compares the dominant object types. - Finds a growing
HashMapkeyed by session ID that is filled on login but never cleaned up on logout. - Replaces it with a bounded cache with time-based expiry and verifies that heap after GC is flat again after the fix.
In other runtimes the tools differ, such as heap snapshots in Chrome DevTools for Node.js, tracemalloc for Python or pprof heap profiles for Go, but the approach is the same: confirm the trend, compare snapshots, find the retaining path.
Best practices
- Give every cache a maximum size and an eviction policy.
- Pair every subscribe with an unsubscribe and every open with a close, ideally through language constructs such as try-with-resources or
defer. - Run soak tests: long load tests at constant traffic reveal leaks that short tests miss.
- Alert on the trend of memory after GC or on predicted time to the memory limit, not only on a static threshold.
- Do not rely on periodic restarts as a fix; they hide the leak and turn it into a recurring incident.
How Obseria helps
Obseria collects runtime metrics such as heap, GC and container memory over OpenTelemetry and can flag rising memory trends with anomaly detection. Continuous profiling records allocation profiles in production, so you can compare a flame graph before and after a release to see which code path allocates and retains memory. For JVM specifics, see Java monitoring.