Safeguard
DevSecOps

Python tracemalloc: Finding Memory Leaks and Why It Matters for Security

How to use Python tracemalloc to trace memory allocations, hunt leaks, and understand why unbounded memory growth is a real availability risk.

Yukti Singhal
Platform Engineer
5 min read

Python tracemalloc is a standard-library module that traces where your program allocates memory, so you can find leaks by comparing snapshots and pinning each block back to the exact line that allocated it. It ships with CPython — no install, no third-party dependency — and it is the right first tool to reach for when a long-running Python service creeps upward in memory until it gets OOM-killed. That slow creep is not just an ops annoyance; unbounded memory growth is a denial-of-service risk, which is why it belongs in a security-minded engineer's toolkit.

Turning tracing on

You start tracing by calling tracemalloc.start(). Do it as early as possible so allocations that happen before the call are not missed:

import tracemalloc

tracemalloc.start()

By default it records a single frame per allocation. To get a real call stack for each block — which you almost always want — pass a frame depth:

tracemalloc.start(25)  # keep up to 25 frames of traceback per allocation

More frames cost more overhead, so keep this modest in anything resembling production. tracemalloc adds measurable slowdown and memory of its own; it is a diagnostic tool, not something to leave running under load.

Snapshots and diffs: the core workflow

The technique that finds leaks is snapshot-and-diff. Take a snapshot, exercise the code path you suspect, take a second snapshot, and compare. The difference shows you what grew.

import tracemalloc

tracemalloc.start(25)

snapshot1 = tracemalloc.take_snapshot()
run_suspect_workload()          # do the thing you think leaks
snapshot2 = tracemalloc.take_snapshot()

top_stats = snapshot2.compare_to(snapshot1, "lineno")
for stat in top_stats[:10]:
    print(stat)

Each line of output tells you a file and line number, the size difference, and the count difference:

myapp/cache.py:88: size=4200 KiB (+4200 KiB), count=51200 (+51200)

That reads as: line 88 of cache.py is holding 4.2 MiB more than before, across 51,200 more objects. A steadily growing count on the same line across repeated cycles is the signature of a leak — objects going in and never being released.

Getting the full traceback

Grouping by "lineno" tells you where memory lives now. Grouping by "traceback" tells you how it got there, which is what you need when the leaking allocation is deep inside a library and the real cause is your calling code:

snapshot = tracemalloc.take_snapshot()
top = snapshot.statistics("traceback")
stat = top[0]
print(f"{stat.count} blocks, {stat.size / 1024:.1f} KiB")
for line in stat.traceback.format():
    print(line)

This prints the complete allocation stack for the single largest consumer, so you can see the chain from your handler down to the allocating call.

The security angle: memory growth is an availability risk

It is tempting to file memory leaks under performance and move on. That undersells them. Availability is one of the three pillars of security, alongside confidentiality and integrity. A memory leak in a request handler is an attacker's lever: if each request permanently retains a little memory, an attacker who can send many requests can drive the process to exhaustion and take the service down — a denial of service that costs them nothing.

The pattern to watch for is any per-request or per-message state that accumulates without a bound: a cache with no eviction, a list that only ever gets appended to, a registry of connections that never removes closed ones. tracemalloc is how you prove which one is the culprit. Once you have identified it, the fix is a bound — an LRU cache with a max size, a periodic cleanup, a weak reference — not just a bigger instance.

For teams thinking about resource-exhaustion defects as a class, the Safeguard Academy covers where availability bugs come from and how to reason about them alongside the more familiar injection and dependency risks.

Filtering out the noise

Real snapshots are cluttered with allocations from the standard library and from tracemalloc itself. Filter them so you see your own code:

snapshot = tracemalloc.take_snapshot().filter_traces((
    tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
    tracemalloc.Filter(False, tracemalloc.__file__),
))

You can also use tracemalloc.get_traced_memory() to grab the current and peak traced size as a quick gauge without a full snapshot — handy for logging a high-water mark from a long-running loop.

When tracemalloc is not enough

tracemalloc traces Python-level allocations. It will not see memory allocated inside C extensions that bypass the Python allocator — NumPy buffers, native database drivers, some serialization libraries. If your leak lives there, tracemalloc will show flat Python usage while RSS climbs, which is itself a useful clue. At that point you reach for OS-level tools or extension-specific profilers. Knowing this boundary saves you from chasing a leak in the wrong layer.

FAQ

Do I need to install Python tracemalloc?

No. tracemalloc is part of the CPython standard library, available since Python 3.4. You import it directly with import tracemalloc — there is nothing to pip install.

How do I find a memory leak with tracemalloc?

Call tracemalloc.start() early, take a snapshot with take_snapshot(), run the code you suspect, take a second snapshot, then call compare_to() on the two and sort by size. Lines whose object count keeps growing across repeated cycles are your leak. Group by "traceback" to see the full allocation stack.

Does tracemalloc slow down my program?

Yes. Tracing adds CPU overhead and consumes memory to store the tracebacks, and deeper frame counts cost more. It is a diagnostic tool for development or a controlled investigation, not something to leave enabled under production load.

Why is a memory leak a security concern?

Availability is part of security. A leak in a request path lets an attacker exhaust memory by sending many requests, causing a denial of service. Treating unbounded memory growth as a security bug — and fixing it with a hard bound rather than a bigger machine — closes that avenue.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.