tracemalloc records where memory blocks managed by Python's allocator were created. It answers a practical question: which lines retain more memory between two points in a run? That evidence is much more actionable than knowing only that a process became larger.
The module is part of the standard library and needs no dependency. It does not, however, measure everything. Resident set size includes the interpreter, native libraries, shared pages, and memory retained by allocators for reuse. A tracemalloc snapshot and operating-system RSS can therefore move differently without either measurement being wrong.
Start tracing at the right time
Enable tracing before importing or running the code under investigation. Blocks allocated before tracing begins have no recorded traceback. A service can start with PYTHONTRACEMALLOC=10 or -X tracemalloc=10; a script or test can use the API:
import tracemalloc
tracemalloc.start(10)
before = tracemalloc.take_snapshot()
data = [{"id": i, "value": str(i)} for i in range(50_000)]
after = tracemalloc.take_snapshot()
for statistic in after.compare_to(before, "lineno")[:10]:
print(statistic)
The argument to start() is the maximum traceback depth stored for each block. More frames help when the allocating line is generic and the meaningful caller is higher in the stack, but they increase the profiler's own CPU and memory cost. Ten frames is a reasonable investigative starting point, not a production default.
take_snapshot() captures blocks that are still allocated at that instant. The example deliberately keeps data alive, so the second snapshot should report growth. In a real service, capture snapshots after equivalent lifecycle points, such as after the first, tenth, and hundredth completed batch.
Read statistics and differences
A snapshot can group traces by lineno, filename, or traceback. lineno is often the clearest first view because it identifies a file and line. traceback separates allocations from the same line according to the call stack that reached it, which is valuable for shared utilities.
Entries returned by compare_to() include size_diff, count_diff, size, and count. A large positive count_diff may reveal an ever-growing collection. A small count with a large size_diff may indicate retained buffers. Read both values instead of treating the first row as an automatic diagnosis.
The comparison can be turned into a repeatable diagnostic:
import tracemalloc
def run_cycles(amount: int) -> None:
for _ in range(amount):
process_batch()
tracemalloc.start(15)
run_cycles(5) # warm-up
baseline = tracemalloc.take_snapshot()
run_cycles(50)
final = tracemalloc.take_snapshot()
for item in final.compare_to(baseline, "filename"):
if item.size_diff > 100_000:
print(item.traceback, item.size_diff, item.count_diff)
Warm-up reduces noise from delayed imports, pool initialization, and the first population of caches. The 100,000-byte threshold is a triage rule, not a universal definition of a leak. Calibrate it to the workload and check whether the increase continues over later rounds.
Filter noise without discarding evidence
Snapshot.filter_traces() accepts Filter and DomainFilter objects. Filters can remove interpreter internals and focus a report on application files:
filters = [
tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
tracemalloc.Filter(False, "*/site-packages/*"),
tracemalloc.Filter(True, "*/my_application/*"),
]
focused = final.filter_traces(filters)
for item in focused.statistics("lineno")[:10]:
print(item)
Exclusions require judgment. A dependency may retain objects because of the way application code calls it, so hiding all of site-packages can remove an important clue. Save or inspect the complete comparison first, then create focused views for navigation.
Measure current usage and peak usage
get_traced_memory() returns current traced bytes and the peak since tracing began. reset_peak() resets only the peak, without stopping tracing. Together they isolate a particular operation:
tracemalloc.start()
load_configuration()
tracemalloc.reset_peak()
build_report()
current, peak = tracemalloc.get_traced_memory()
print(f"current={current / 1024:.1f} KiB")
print(f"peak={peak / 1024:.1f} KiB")
Peak measurement is useful for jobs that release data on completion but temporarily exceed a memory limit. Snapshots are better for finding allocations that remain. These are complementary questions rather than competing metrics.
Interpret results correctly
One comparison does not prove a leak. Imports, bounded caches, connection pools, compiled regular expressions, and interpreter data may grow once and stabilize. Run identical rounds, exclude warm-up, and look for a trend. A diagnostic may call gc.collect() before snapshots to reduce noise from collectable cycles, but forced collection changes normal behavior and should not become a cosmetic fix.
Confirm which references are expected to remain. A global list, registered callback, unfinished asynchronous task, or unbounded cache can keep objects reachable. tracemalloc points to where memory was allocated; it does not show the reference chain retaining an object. The gc module, reference inspection, or an object profiler can answer that separate question.
Do not label an unchanged RSS as a leak merely because del and garbage collection did not make the operating-system number fall. Python may return blocks to its own pools without returning pages immediately to the OS. Conversely, a C extension can grow RSS while contributing little useful detail to Python allocation snapshots.
Save snapshots for offline analysis
Snapshots support dump() and Snapshot.load(), which helps when the affected environment has no interactive profiler:
snapshot = tracemalloc.take_snapshot()
snapshot.dump("/tmp/after-load.snapshot")
## Later, in an analysis process:
loaded = tracemalloc.Snapshot.load("/tmp/after-load.snapshot")
for item in loaded.statistics("traceback")[:5]:
print(item)
Snapshot files can expose source paths, module names, and application details. Treat them as diagnostic artifacts, restrict access, and never publish them. Comparisons across processes are meaningful only when code, Python, dependencies, configuration, and workload are sufficiently similar.
A practical investigation sequence
First reproduce growth with controlled inputs. Start tracing before warm-up, capture a baseline, and repeat equivalent units of work. Compare by line and traceback, inspect the largest positive differences, and determine whether they continue to grow. Finally, correlate the result with RSS, workload counters, queue sizes, and cache limits.
Use it carefully in tests and production
A memory regression test must tolerate small interpreter variations. Do not require an exact byte count. Repeat the operation, derive a margin from measurements, and check for a clearly abnormal trend. Isolate the run from unrelated plugins, logging, and parallel tests. Results from different Python versions should not be treated as though their allocator behavior were identical.
In production, prefer a short, controlled observation window. Capturing deep stacks for hours adds overhead to a process that may already be under pressure. Record application version, input volume, relevant configuration, and snapshot times. Without that context, a large difference may simply represent different workloads.
Call tracemalloc.stop() after collection. clear_traces() removes current traces and establishes room for a fresh baseline, but old snapshots can no longer be compared meaningfully with the cleared state. Preserve only the required artifact in a protected location before clearing it.
When reporting findings, state whether each number is current traced size, peak, a snapshot difference, or process RSS. Treat suspicious lines as investigation leads rather than proven blame. The final validation should show that the same workload stabilizes across repeated rounds after a fix and that functional behavior remains correct.
Combine this with cProfile for CPU profiling only when response time is also under investigation. CPU and memory are separate dimensions. For intentional retention, review cachetools limits and TTL.
The official tracemalloc documentation, accessed July 28, 2026, covers startup options, snapshots, filters, domains, and peak measurements. Consult the documentation matching the Python version actually deployed. With equivalent measurement points and a clear distinction between traced heap and process memory, tracemalloc turns a vague symptom into testable hypotheses.