What Is a Memory Leak? RAM That Checks In and Never Checks Out

- Memory leaks happen when heap memory is allocated but the pointer is lost, so it can never be freed and keeps growing until the process crashes.
- Stack memory is leak-proof: frames are reclaimed automatically on return; leaks only live on the heap.
- GC languages aren't immune: holding a live reference longer than needed is a reference leak the garbage collector cannot fix.
- Event listeners and closures are the most common JavaScript leak pattern; explicit
removeEventListeneror{ once: true }prevents unbounded growth. - Reference cycles (A → B → A) defeat reference counting; Python's cyclic GC finds them, but older or simpler runtimes silently leak them.
- In coding interviews, unbounded caches and BFS without visited sets are memory leaks by another name; "what is the space complexity" is partly this question.
- Detection tools: Valgrind and AddressSanitizer for C/C++,
tracemallocfor Python, VisualVM or Java Flight Recorder for JVM languages.
Your program has a guest. It showed up, used the place, and never left. Now it has friends. They are also not leaving. Your server runs out of RAM at 3 AM and someone gets a page. That is a memory leak.
More precisely: a memory leak happens when a program allocates memory on the heap, loses the ability to free it, and keeps running anyway. The memory stays reserved. Nothing else can use it. It just sits there, taking up space, doing nothing, like a process you forgot to kill.
This is different from using a lot of memory deliberately. A cache holding a million entries is expensive but intentional. A leak is memory that accumulates without any corresponding release, growing as long as the process lives.
Run a leaking process long enough and one of three things happens: it slows down as the OS swaps to disk, it crashes with an out-of-memory error, or it destabilizes other processes on the same host. In a long-running service that never restarts, even a small leak per request adds up fast.
Stack vs. Heap: Where Leaks Actually Live
Every running process has two main memory regions.
The stack handles function calls. When you call a function, the runtime pushes a frame onto the stack. That frame holds local variables, parameters, and the return address. When the function returns, the frame is automatically popped. No manual cleanup. No leaks. The stack is the responsible adult in this story.
The heap is where long-lived data lives: objects, arrays, data structures built at runtime. Someone or something has to decide when heap-allocated memory is no longer needed and free it. In C and C++, that someone is you. In Python, Java, Go, and most modern languages, it is the garbage collector. In both cases, things go wrong.
Memory leaks happen on the heap. Stack memory is reclaimed automatically on return. For a full breakdown of how these two regions work and why they have different lifetimes, stack vs. heap memory covers the mechanics in detail.
The Classic C Leak: An Eternal Promise
In C, you request heap memory with malloc and release it with free. Forget the free call and you have a leak.
#include <stdlib.h> #include <stdio.h> void process_request() { int *buffer = (int *)malloc(1024 * sizeof(int)); // ... use buffer ... // "I'll free it later." Narrator: they did not. } int main() { for (int i = 0; i < 1000000; i++) { process_request(); } return 0; }
Each call to process_request allocates 4 KB. The function returns, the local pointer buffer disappears from the stack. The heap memory it pointed to stays allocated. Still reserved. The OS will not reclaim it until the entire process exits.
After a million calls, that is roughly 4 GB consumed. The memory just keeps growing until something breaks.
The more treacherous real-world variant: an error path returns early without freeing a buffer allocated earlier in the function. The happy path is fine. The error path leaks. Tests never trigger the error path under load, so the leak ships to production and quietly destroys your service over six months.
Garbage Collection Does Not Save You
Here is where Python and Java developers develop a false sense of security. The garbage collector tracks references and frees memory when nothing points to an object anymore.
Except when it does not.
A garbage-collected leak is a reference leak: you hold a reference to an object longer than you need to, so the GC cannot collect it. The memory is still "in use" from the runtime's perspective, even though your program will never touch it again.
cache = {} def process(key, value): result = expensive_computation(value) cache[key] = result # stored forever, like regrets return result
The cache dictionary grows every time process is called. If keys keep changing (user IDs, request IDs, timestamps), the cache grows without bound. Python's GC will not collect those entries because cache still holds references to them. The leak is in the design, not the language runtime.
This pattern shows up constantly in real systems: unbounded caches with no eviction, event listener registries that never deregister, global data structures that accumulate entries across the lifetime of the process. The GC did its job. You just handed it an object it could not legally touch.
For a full explanation of how Python decides when an object's lifetime is over, Python reference counting covers the mechanics across collection strategies.
Reference Cycles: They're Holding Each Other Hostage
Reference counting has a well-known weakness. If object A holds a reference to object B, and B holds a reference back to A, neither will ever have a reference count of zero, even if nothing else in the program can reach either object.
class Node: def __init__(self): self.partner = None a = Node() b = Node() a.partner = b b.partner = a del a del b # Both objects still alive: their reference counts are 1, not 0
Python's cyclic GC exists specifically to find and collect these cycles. But it runs periodically, not instantly, so cycles can accumulate between runs. In older or simpler runtimes without a cyclic collector, these objects are simply never freed.
The canonical production example is a doubly-linked list or a parent-child object graph where children hold references back to their parents. Release the root, forget the back-references, and the entire structure leaks. You thought you cleaned up. You did not.
JavaScript and the Event Listener Trap
In JavaScript, the most common leak pattern is event listeners and closures doing something you did not explicitly ask them to do.
function setupButton() { const largeData = new Array(100000).fill("data"); document.getElementById("btn").addEventListener("click", () => { console.log(largeData.length); // closure captures largeData }); } // Called once per page navigation, never cleaned up setupButton();
The click listener closes over largeData. Even if setupButton returns, even if nothing else in your code references largeData, the event listener keeps it alive. Call setupButton on every navigation without calling removeEventListener first and you get a new listener and a new largeData each time. The old ones never get collected.
The fix is explicit cleanup: call removeEventListener before re-attaching, or use { once: true } for one-shot handlers. If you have ever watched a browser tab's memory climb steadily over 30 minutes of normal use, this is almost certainly why.
Node.js server processes have the same problem with EventEmitter listeners. The default maximum listener count (10 per emitter) exists to surface this bug with a warning before it becomes a 3 AM incident.
Why Interviews Ask About This
Memory leaks come up in two distinct interview contexts: coding rounds and system design.
In coding rounds, interviewers watch for space complexity awareness. If you implement an LRU cache with a bug in the eviction policy, the cache grows without bound. That is a memory leak by another name. If you run a BFS and fail to mark nodes as visited, you allocate queue entries for nodes you have already processed. The question "what is the space complexity of your solution" is partly asking whether you know which memory lives as long as the call runs versus which lives as long as the process runs.
In system design, a memory leak in a long-running service is a production incident waiting to happen. A caching layer without a maximum size and eviction policy leaks. A notification system with a subscriber registry and no deregistration path leaks. These are not edge cases. They are design flaws that a senior engineer is expected to catch.
SpaceComplexity runs voice-based mock interviews where you get pushed to explain not just what your solution does but why it handles edge cases like unbounded growth. Being asked "what happens to memory usage after 10 million requests" while coding out loud is a completely different pressure than writing about it.
How to Find a Memory Leak
The approach is the same across languages: measure allocation over time and find what keeps growing.
In C/C++, Valgrind's memcheck tool runs your program and reports every allocation that was never freed. AddressSanitizer (built into Clang and GCC with -fsanitize=address) catches leaks at runtime with minimal overhead. These are standard tools in any serious C/C++ workflow.
In Python, tracemalloc lets you take snapshots of heap allocations and compare them:
import tracemalloc tracemalloc.start() # ... run the suspect code ... snapshot = tracemalloc.take_snapshot() top_stats = snapshot.statistics("lineno") for stat in top_stats[:10]: print(stat)
A site whose allocation count keeps climbing across snapshots is a leak candidate. In Java and JVM languages, heap profilers like VisualVM or Java Flight Recorder show object instance counts over time. A class whose count keeps climbing without a corresponding drop is worth investigating.
The fix is almost always one of three things: free the memory explicitly in C/C++, drop the lingering reference in GC languages, or add a maximum size and eviction strategy to an unbounded container.