Sitemap

Stop the Leak! A Complete Guide to Memory Leaks in C++ and Go

8 min readMay 6, 2026

--

Memory leaks are among the most common and frustrating issues in software development. They cause slowdowns, increasing resource usage, instability, and sometimes full application failure. This guide provides a clear, practical explanation of what memory leaks are, how they occur, how to detect them, and how to fix them in C++ and Golang.

Press enter or click to view image in full size
Fixing Memory Leaks (Generated by Microsoft Bing AI)

What Is a Memory Leak?

A memory leak occurs when a program allocates memory but fails to release it when no longer needed.

Press enter or click to view image in full size
Memory Leaks illustration from this Linkedin article

Two main situations lead to leaks:

1. True leaks: Memory becomes unreachable and cannot be freed.
Example: calling malloc() in C/C++ but never calling free().

2. Logical leaks: Memory is still referenced but should not be.
Example: a cached map grows indefinitely, or a goroutine waits forever.

Get Sylvain Tiset’s stories in your inbox

Join Medium for free to get updates from this writer.

Both types result in increasing memory usage over time.

Memory Management Basics

Stack vs Heap

  • Stack: automatic memory, freed when functions return
  • Heap: dynamic memory, manually managed (C/C++) or garbage-collected (Go)
Stack vs Heap Structure from baeldung.com

Memory life cycle

Regardless of the programming language, the memory life cycle is pretty much always the same:

Memory life cycle from this Medium article
  1. Allocate the memory needed
  2. Use the allocated memory
  3. Release the allocated memory when it is not required anymore

Manual vs Automatic Management

  • C/C++: direct control (new, delete, malloc, free) implies high risk of leaks
  • Go: garbage-collected implies fewer leaks, but leaks still occur through retained references or goroutines

Garbage collection removes unreachable memory, but it cannot guess when data should be discarded. As long as something references it, the GC must keep it.

How Memory Leaks Happen

Common Causes in C++

  • Missing delete or free()
void leak() {
int* p = new int(42);
// ...
// forgot: delete p;
}
  • Early returns or exception paths skipping deallocation
void leak(bool fail) {
int* p = new int(10);

if (fail) {
return; // 'p' is leaked on this branch
}

delete p;
}
  • Double allocation without freeing the previous pointer
void leak() {
int* p = new int(1);
p = new int(2); // previous allocation lost => leak
delete p;
}
  • Not following RAII (Resource Acquisition Is Initialization)
class FileWrapper {
public:
FILE* f;
FileWrapper(const char* path) { f = fopen(path, "w"); }

void writeSomething() { fprintf(f, "hello\n"); }
// missing fclose(f) in destructor => leak of file handle
};
  • shared_ptr reference cycles (two objects owning each other)
#include <memory>

struct B; // forward declaration

struct A {
std::shared_ptr<B> b;
};

struct B {
std::shared_ptr<A> a;
};

void leak() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();

a->b = b;
b->a = a; // cycle: use_count keeps both objects alive forever
}
  • Misuse of new[], malloc, or realloc
void leak() {
int* arr = new int[100];
// missing delete[] arr;
}

void leak() {
int* p = (int*)malloc(100);
delete p; // wrong deallocator → UB + leak
}

void leak() {
char* buf = (char*)malloc(10);
buf = (char*)realloc(buf, 20);
// If realloc fails, original buf is lost → leak
}

Note: Resource Acquisition Is Initialization: a C++ design principle where a resource (such as memory, a file handle, a mutex, or a network connection) is acquired and released automatically through an object’s lifetime.
The idea is simple:
- A resource is acquired in the constructor of an object.
- The resource is automatically released in the object’s destructor when it goes out of scope.

This ensures that resources are always cleaned up, even if a function returns early or an exception occurs. RAII eliminates the need for manual delete or free() calls and is the foundation of safe memory and resource management in modern C++.

#include <cstdio>
#include <memory>

class File {
public:
File(const char* path, const char* mode) {
file_ = std::fopen(path, mode);
}

~File() {
if (file_) {
std::fclose(file_);
}
}

FILE* get() const { return file_; }

private:
FILE* file_;
};

void process() {
File f("data.txt", "r"); // resource acquired

FILE* file = f.get();
// ... do something ...

} // f goes out of scope → destructor automatically calls fclose()

// Note: This can also be achieved with smart pointers:
void example() {
auto buffer = std::make_unique<int[]>(100);
// memory is automatically freed when buffer goes out of scope
}

Common Causes in Golang

  • Goroutine leaks
func process(ch <-chan int) {
go func() {
for v := range ch {
// do something
}
}()
}
  • Channels that block forever
func producer(ch chan int) {
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
// no close(ch) → consumers block
}()
}
  • Slices retaining large underlying arrays
data := make([]byte, 10_000_000)
small := data[:10] // small slice holding reference to 10MB array
  • Keeping references in long-lived objects (like global caches)
var global []*bigStruct

func addLeak() {
s := make([]byte, 10<<20) // 10MB
global = append(global, s) // permanent retention
}
var cache = map[string][]byte{}

func store(key string, data []byte) {
cache[key] = data // grows forever
}
  • Timers or tickers not stopped
ticker := time.NewTicker(time.Minute)
go func() {
for range ticker.C {
// work
}
}()
  • Contexts ignored or never cancelled
for i := 0; i < 1000; i++ {
// Using context.Background() means these contexts NEVER end.
worker(context.Background())
}

Detecting Memory Leaks

General Signs of a Leak

These are general signs that indicate a potential memory leak:

  • Gradually increasing RAM usage (Resident Set Size)
  • GC running more frequently (in GC languages like Golang)
  • Latency spikes
  • Crashes or Out Of Memory (OOM) kills
  • Production services getting slower over time

RSS (Resident Set Size) represents how much physical RAM a process is currently using. If a program leaks memory, its RSS tends to increase slowly over time, even when the workload stays the same. This happens because the application keeps allocating memory without releasing it, and the operating system cannot reclaim the leaked memory.
Virtual Memory on the other hand represents the memory a program believes it has.

Virtual Memory vs Resident Set Size from docs.hpc.qmul.ac.uk

C++ Tools

  • Valgrind (memcheck): Detects memory leaks, invalid reads/writes, and improper memory usage by running the program in a monitored environment.
  • AddressSanitizer (ASan): A fast compiler-based tool that catches memory errors such as buffer overflows, use-after-free, and out-of-bounds accesses.
  • LeakSanitizer (LSan): Reports memory leaks at program exit by tracking allocations and their lifetimes, often used together with ASan.
  • Heaptrack: Profiles memory allocations over time and identifies hotspots and growing memory patterns using visual graphs.
Press enter or click to view image in full size
Heaptrack illustration from linux.developpez.com
  • Deleaker: A memory profiler for C++ that detects memory leaks, GDI and USER object leaks, and handle leaks. It provides a complete call stack for each allocation and integrates with popular IDEs, including Visual Studio, RAD Studio, and Qt Creator. Official website | Video tutorial
Press enter or click to view image in full size
Deleaker illusatration from Deleaker

Golang Tools

  • pprof: Go’s built-in profiler that collects heap, CPU, and goroutine profiles to identify where memory is allocated or retained.
Press enter or click to view image in full size
pprof illustration from honeycomb.io
  • runtime.ReadMemStats: Provides detailed runtime memory statistics programmatically, useful for debugging or exporting custom metrics.
  • GODEBUG=gctrace=1: Enables detailed garbage collector logs that reveal allocation patterns, GC frequency, and memory pressure.
  • Grafana dashboards: Visualize memory metrics (heap usage, GC pauses, goroutines) from Prometheus or other exporters to spot leaks in production.
Press enter or click to view image in full size
Graphana dashboard from community.graphana.com

Fixing Memory Leaks in C++

C++ memory safety problems mainly come from manual management. The most robust approach is to avoid raw pointers as much as possible.

Apply RAII & Use Smart Pointers Correctly

RAII ensures that resources are freed when objects go out of scope.

std::unique_ptr<Foo> ptr = std::make_unique<Foo>();

RAII and smart pointers avoids forgetting to delete memory. As a reminder, here are the three main smart pointers to be used:

  • unique_ptr: For exclusive ownership. Prevents forgetting to free memory.
  • shared_ptr: For shared ownership.
  • weak_ptr: Breaks cycles.

More informations on smart pointers in this previous article.

Use tools

If it becomes hard to pinpoint a memory leak by hand, the best way forward is to use the tools mentioned earlier and start debugging with their help.

Fixing Memory Leaks in Golang

Go leaks are usually logical — the garbage collector cannot free memory that remains referenced or memory owned by stuck goroutines.

Goroutine Leaks

// Add context cancellation
func process(ctx context.Context, ch <-chan int) {
go func() {
for {
select { // Use select with a timeout
case v := <-ch:
// handle v
case <-ctx.Done():
return
}
}
}()
}
// Don't forget to close channels in the callers !

Channels that block forever

func producerSafe(ch chan int) {
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch) // signals completion
}()
}

Slice Backing Array Leaks

// Copy the needed part instead
small := append([]byte(nil), data[:10]...)

Retaining References in Long-Lived Structures & Global Cache Leaks

var global []int // or another lightweight type

func addSafe() {
s := make([]byte, 10<<20)
checksum := int(s[0]) // or extract needed fields
global = append(global, checksum)
}
var cache = make(map[string][]byte)
var cacheOrder []string
const maxEntries = 100

func storeSafe(key string, data []byte) {
if len(cache) >= maxEntries {
oldest := cacheOrder[0]
cacheOrder = cacheOrder[1:]
delete(cache, oldest)
}

cache[key] = data
cacheOrder = append(cacheOrder, key)
}

Timers and Tickers Leaks

ticker.Stop()

Contexts ignored or never cancelled

for i := 0; i < 1000; i++ {
ctx, cancel := context.WithCancel(context.Background())
worker(ctx)

time.AfterFunc(100*time.Millisecond, cancel) // guarantee cleanup
}

Best practices

Adopting solid best practices greatly reduces the likelihood of memory leaks and makes them easier to track down when they appear.

Regular code reviews help catch risky patterns early, while load testing exposes leaks that only emerge under sustained or high-pressure workloads.

When hunting a leak, patience and a methodical, divide-and-conquer approach (dichotomy) remain essential to isolating the faulty component.

Keeping dependencies up to date is equally important, as libraries often ship memory-related fixes.

Finally, static analysis tools such as SonarQube can highlight suspicious constructs and provide early warnings for code that may leak.

In the end, tracking down memory leaks is one of the most difficult task for a computer engineer. It requires patience, persistence, and a bit of detective work.

Hoping memory leaks have no more secrets to you. If you liked this article, feel free to clap it, comment it or share it. To stay updated with my content, you can follow me on Medium.

--

--