[{"data":1,"prerenderedAt":1214},["ShallowReactive",2],{"page-\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002F":3},{"id":4,"title":5,"body":6,"description":1179,"extension":1180,"meta":1181,"navigation":347,"path":1210,"seo":1211,"stem":1212,"__hash__":1213},"content\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Findex.md","Memory Profiling with tracemalloc",{"type":7,"value":8,"toc":1162},"minimark",[9,34,39,96,100,136,151,305,309,314,326,361,368,400,404,413,441,445,461,481,488,502,517,521,532,584,588,606,644,648,661,795,807,811,832,893,897,933,937,1068,1072,1084,1095,1107,1120,1124,1151,1158],[10,11,12,13,17,18,22,23,26,27,30,31,33],"p",{},"A long-running worker creeps from 200 MB to 2 GB over a day and gets OOM-killed; a pytest session that should be flat grows with every test until CI runners die. ",[14,15,16],"code",{},"top"," tells you ",[19,20,21],"em",{},"that"," memory is growing but not ",[19,24,25],{},"which line"," is responsible. ",[14,28,29],{},"tracemalloc",", in the standard library since Python 3.4, records the Python call stack for every allocation, so you can attribute live bytes to exact source lines and call paths, diff two points in time, and assert ceilings in tests. This guide covers driving ",[14,32,29],{}," end to end: configuring frame depth, taking and grouping snapshots, filtering noise, and wiring memory assertions into pytest.",[35,36,38],"h2",{"id":37},"prerequisites","Prerequisites",[40,41,42,64,83,89],"ul",{},[43,44,45,49,50,52,53,49,56,59,60,63],"li",{},[46,47,48],"strong",{},"Python 3.4+"," for ",[14,51,29],{}," itself; ",[46,54,55],{},"3.6+",[14,57,58],{},"Snapshot.compare_to"," ordering by ",[14,61,62],{},"size_diff"," to behave as documented here.",[43,65,66,67,70,71,74,75,78,79,82],{},"A way to start tracing ",[19,68,69],{},"before"," the allocations you care about — either ",[14,72,73],{},"tracemalloc.start()"," early in the process, or the ",[14,76,77],{},"PYTHONTRACEMALLOC=N"," environment variable \u002F ",[14,80,81],{},"-X tracemalloc=N"," flag to set frame depth at launch.",[43,84,85,88],{},[46,86,87],{},"pytest 6+"," if you intend to gate memory in CI with the fixture pattern below.",[43,90,91,92,95],{},"Awareness that tracing adds CPU and memory overhead proportional to ",[14,93,94],{},"nframe","; never leave it on in production hot paths.",[35,97,99],{"id":98},"core-concept","Core concept",[10,101,102,104,105,108,109,111,112,115,116,119,120,123,124,127,128,131,132,135],{},[14,103,29],{}," hooks Python's memory allocators. Once ",[14,106,107],{},"tracemalloc.start(nframe)"," runs, every subsequent allocation is recorded with up to ",[14,110,94],{}," stack frames. A ",[46,113,114],{},"snapshot"," (",[14,117,118],{},"take_snapshot()",") is an immutable copy of all currently tracked allocations at that instant. You then ask the snapshot to aggregate its traces into ",[46,121,122],{},"statistics",", grouped either by ",[14,125,126],{},"'lineno'"," (one entry per source line) or ",[14,129,130],{},"'traceback'"," (one entry per distinct call path). ",[14,133,134],{},"filter_traces"," removes entries you do not care about — the standard library, importlib, tracemalloc's own frames — before you read the numbers.",[10,137,138,139,142,143,146,147,150],{},"The leak-hunting workflow is a pipeline: capture a baseline snapshot, exercise the code, capture a second snapshot, and ",[14,140,141],{},"compare_to"," the baseline to surface the lines whose retained bytes ",[19,144,145],{},"grew",". The single number ",[14,148,149],{},"get_traced_memory()"," (current, peak) is the cheap gate for tests. The diagram traces that pipeline.",[152,153,156,301],"figure",{"className":154},[155],"diagram",[157,158,165,166,165,170,165,174,165,184,165,195,165,201,165,205,165,209,165,213,165,216,165,221,165,225,165,228,165,231,165,235,165,238,165,244,165,248,165,252,165,255,165,259,165,262,165,266,165,270,165,274,165,280,165,283,165,287,165,291,165,295,165,298],"svg",{"viewBox":159,"role":160,"ariaLabelledBy":161,"xmlns":164},"0 0 820 360","img",[162,163],"tmalloc-title","tmalloc-desc","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[167,168,169],"title",{"id":162},"tracemalloc snapshot and compare pipeline",[171,172,173],"desc",{"id":163},"Start tracing, take a baseline snapshot, run the workload, take a second snapshot, then compare and read the top growing lines.",[175,176,183],"text",{"x":177,"y":178,"textAnchor":179,"fontSize":180,"fontWeight":181,"fill":182},"410","34","middle","19","700","#3d405b","The tracemalloc pipeline",[185,186],"rect",{"x":187,"y":188,"width":189,"height":190,"rx":191,"fill":192,"stroke":193,"strokeWidth":194},"30","64","160","62","12","#fffdf8","#e07a5f","2",[175,196,200],{"x":197,"y":198,"textAnchor":179,"fontSize":199,"fontWeight":181,"fill":182},"110","90","13","start(nframe)",[175,202,204],{"x":197,"y":197,"textAnchor":179,"fontSize":203,"fill":182},"11","hook allocators",[185,206],{"x":207,"y":188,"width":189,"height":190,"rx":191,"fill":192,"stroke":208,"strokeWidth":194},"222","#81b29a",[175,210,212],{"x":211,"y":198,"textAnchor":179,"fontSize":199,"fontWeight":181,"fill":182},"302","snapshot A",[175,214,215],{"x":211,"y":197,"textAnchor":179,"fontSize":203,"fill":182},"baseline",[185,217],{"x":218,"y":188,"width":189,"height":190,"rx":191,"fill":219,"stroke":182,"strokeWidth":220},"414","#f4f1de","1.5",[175,222,224],{"x":223,"y":198,"textAnchor":179,"fontSize":199,"fontWeight":181,"fill":182},"494","run workload",[175,226,227],{"x":223,"y":197,"textAnchor":179,"fontSize":203,"fill":182},"allocations happen",[185,229],{"x":230,"y":188,"width":189,"height":190,"rx":191,"fill":192,"stroke":208,"strokeWidth":194},"606",[175,232,234],{"x":233,"y":198,"textAnchor":179,"fontSize":199,"fontWeight":181,"fill":182},"686","snapshot B",[175,236,237],{"x":233,"y":197,"textAnchor":179,"fontSize":203,"fill":182},"after",[239,240],"line",{"x1":241,"y1":242,"x2":243,"y2":242,"stroke":182,"strokeWidth":194},"190","95","220",[245,246],"polygon",{"points":247,"fill":182},"220,95 210,90 210,100",[239,249],{"x1":250,"y1":242,"x2":251,"y2":242,"stroke":182,"strokeWidth":194},"382","412",[245,253],{"points":254,"fill":182},"412,95 402,90 402,100",[239,256],{"x1":257,"y1":242,"x2":258,"y2":242,"stroke":182,"strokeWidth":194},"574","604",[245,260],{"points":261,"fill":182},"604,95 594,90 594,100",[185,263],{"x":207,"y":264,"width":265,"height":188,"rx":191,"fill":192,"stroke":193,"strokeWidth":194},"186","544",[175,267,269],{"x":223,"y":268,"textAnchor":179,"fontSize":199,"fontWeight":181,"fill":182},"212","B.compare_to(A, 'lineno')",[175,271,273],{"x":223,"y":272,"textAnchor":179,"fontSize":203,"fill":182},"232","sorted by size_diff, biggest growth first",[239,275],{"x1":233,"y1":276,"x2":233,"y2":264,"stroke":182,"strokeWidth":277,"strokeDashArray":278},"126","1.2",[279,279],"4",[239,281],{"x1":211,"y1":276,"x2":211,"y2":264,"stroke":182,"strokeWidth":277,"strokeDashArray":282},[279,279],[185,284],{"x":207,"y":285,"width":265,"height":286,"rx":191,"fill":219,"stroke":182,"strokeWidth":220},"278","58",[175,288,290],{"x":223,"y":289,"textAnchor":179,"fontSize":199,"fontWeight":181,"fill":182},"303","top stats - the leaking lines",[175,292,294],{"x":223,"y":293,"textAnchor":179,"fontSize":203,"fill":182},"323","+12.4 MiB  cache.py:88  (90342 blocks)",[239,296],{"x1":223,"y1":297,"x2":223,"y2":285,"stroke":182,"strokeWidth":194},"250",[245,299],{"points":300,"fill":182},"494,278 489,268 499,268",[302,303,304],"figcaption",{},"Two snapshots bracket the workload; compare_to diffs them and sorts by size_diff so the lines retaining the most new memory rise to the top.",[35,306,308],{"id":307},"step-by-step-implementation","Step-by-step implementation",[310,311,313],"h3",{"id":312},"_1-start-tracing-with-the-right-frame-depth","1. Start tracing with the right frame depth",[10,315,316,318,319,321,322,325],{},[14,317,107],{}," begins recording. ",[14,320,94],{}," is the number of stack frames stored per allocation. The default of ",[14,323,324],{},"1"," tells you the allocating line but not how it was reached; raise it when allocations funnel through a shared helper and you need the caller.",[327,328,333],"pre",{"className":329,"code":330,"language":331,"meta":332,"style":332},"language-python shiki shiki-themes github-light github-dark","import tracemalloc\n\n# Record up to 25 frames so we can group by full call path later.\ntracemalloc.start(25)\n","python","",[14,334,335,342,349,355],{"__ignoreMap":332},[336,337,339],"span",{"class":239,"line":338},1,[336,340,341],{},"import tracemalloc\n",[336,343,345],{"class":239,"line":344},2,[336,346,348],{"emptyLinePlaceholder":347},true,"\n",[336,350,352],{"class":239,"line":351},3,[336,353,354],{},"# Record up to 25 frames so we can group by full call path later.\n",[336,356,358],{"class":239,"line":357},4,[336,359,360],{},"tracemalloc.start(25)\n",[10,362,363,364,367],{},"To enable tracing from the very first allocation — before your own code runs — set the environment instead of calling ",[14,365,366],{},"start()",":",[327,369,373],{"className":370,"code":371,"language":372,"meta":332,"style":332},"language-bash shiki shiki-themes github-light github-dark","PYTHONTRACEMALLOC=25 python worker.py        # or: python -X tracemalloc=25 worker.py\n","bash",[14,374,375],{"__ignoreMap":332},[336,376,377,381,385,389,393,396],{"class":239,"line":338},[336,378,380],{"class":379},"sVt8B","PYTHONTRACEMALLOC",[336,382,384],{"class":383},"szBVR","=",[336,386,388],{"class":387},"sZZnC","25",[336,390,392],{"class":391},"sScJk"," python",[336,394,395],{"class":387}," worker.py",[336,397,399],{"class":398},"sJ8bj","        # or: python -X tracemalloc=25 worker.py\n",[310,401,403],{"id":402},"_2-take-a-snapshot","2. Take a snapshot",[10,405,406,408,409,412],{},[14,407,118],{}," freezes the current set of tracked allocations into an immutable ",[14,410,411],{},"Snapshot",". It is cheap to hold and safe to pickle, so you can capture one, run work, capture another, and diff offline.",[327,414,416],{"className":329,"code":415,"language":331,"meta":332,"style":332},"import tracemalloc\n\ntracemalloc.start(25)\ndata = [bytes(1024) for _ in range(10_000)]   # ~10 MB of work\nsnapshot = tracemalloc.take_snapshot()\n",[14,417,418,422,426,430,435],{"__ignoreMap":332},[336,419,420],{"class":239,"line":338},[336,421,341],{},[336,423,424],{"class":239,"line":344},[336,425,348],{"emptyLinePlaceholder":347},[336,427,428],{"class":239,"line":351},[336,429,360],{},[336,431,432],{"class":239,"line":357},[336,433,434],{},"data = [bytes(1024) for _ in range(10_000)]   # ~10 MB of work\n",[336,436,438],{"class":239,"line":437},5,[336,439,440],{},"snapshot = tracemalloc.take_snapshot()\n",[310,442,444],{"id":443},"_3-group-statistics-by-lineno-or-traceback","3. Group statistics by lineno or traceback",[10,446,447,450,451,454,455,457,458,460],{},[14,448,449],{},"snapshot.statistics(key_type)"," returns a list of ",[14,452,453],{},"Statistic"," objects sorted largest-first. Use ",[14,456,126],{}," to collapse everything allocated on the same line into one entry, or ",[14,459,130],{}," to keep distinct call paths separate.",[327,462,464],{"className":329,"code":463,"language":331,"meta":332,"style":332},"for stat in snapshot.statistics(\"lineno\")[:5]:\n    # stat.size is bytes retained; stat.count is the number of blocks\n    print(f\"{stat.size \u002F 1024:8.1f} KiB  {stat.count:>7} blocks  {stat.traceback[0]}\")\n",[14,465,466,471,476],{"__ignoreMap":332},[336,467,468],{"class":239,"line":338},[336,469,470],{},"for stat in snapshot.statistics(\"lineno\")[:5]:\n",[336,472,473],{"class":239,"line":344},[336,474,475],{},"    # stat.size is bytes retained; stat.count is the number of blocks\n",[336,477,478],{"class":239,"line":351},[336,479,480],{},"    print(f\"{stat.size \u002F 1024:8.1f} KiB  {stat.count:>7} blocks  {stat.traceback[0]}\")\n",[327,482,486],{"className":483,"code":485,"language":175,"meta":332},[484],"language-text"," 10240.0 KiB    10000 blocks  worker.py:4\n",[14,487,485],{"__ignoreMap":332},[10,489,490,491,493,494,497,498,501],{},"Switching to ",[14,492,130],{}," and printing ",[14,495,496],{},"stat.traceback.format()"," shows the full path that reached the allocating line — essential when a generic ",[14,499,500],{},"list.append"," is the named site but the cause is one specific caller.",[327,503,505],{"className":329,"code":504,"language":331,"meta":332,"style":332},"top = snapshot.statistics(\"traceback\")[0]\nprint(\"\\n\".join(top.traceback.format()))   # full call stack for the biggest allocation\n",[14,506,507,512],{"__ignoreMap":332},[336,508,509],{"class":239,"line":338},[336,510,511],{},"top = snapshot.statistics(\"traceback\")[0]\n",[336,513,514],{"class":239,"line":344},[336,515,516],{},"print(\"\\n\".join(top.traceback.format()))   # full call stack for the biggest allocation\n",[310,518,520],{"id":519},"_4-filter-out-noise","4. Filter out noise",[10,522,523,524,527,528,531],{},"Raw snapshots are dominated by the import machinery and tracemalloc's own bookkeeping. ",[14,525,526],{},"snapshot.filter_traces([...])"," returns a new snapshot keeping only matching frames. Negative filters (",[14,529,530],{},"inclusive=False",") drop frames; positive filters keep only your module.",[327,533,535],{"className":329,"code":534,"language":331,"meta":332,"style":332},"import tracemalloc\n\nfiltered = snapshot.filter_traces((\n    tracemalloc.Filter(False, \"\u003Cfrozen importlib._bootstrap>\"),\n    tracemalloc.Filter(False, tracemalloc.__file__),   # drop tracemalloc's own frames\n    tracemalloc.Filter(False, \"\u003Cunknown>\"),\n))\nfor stat in filtered.statistics(\"lineno\")[:5]:\n    print(stat)\n",[14,536,537,541,545,550,555,560,566,572,578],{"__ignoreMap":332},[336,538,539],{"class":239,"line":338},[336,540,341],{},[336,542,543],{"class":239,"line":344},[336,544,348],{"emptyLinePlaceholder":347},[336,546,547],{"class":239,"line":351},[336,548,549],{},"filtered = snapshot.filter_traces((\n",[336,551,552],{"class":239,"line":357},[336,553,554],{},"    tracemalloc.Filter(False, \"\u003Cfrozen importlib._bootstrap>\"),\n",[336,556,557],{"class":239,"line":437},[336,558,559],{},"    tracemalloc.Filter(False, tracemalloc.__file__),   # drop tracemalloc's own frames\n",[336,561,563],{"class":239,"line":562},6,[336,564,565],{},"    tracemalloc.Filter(False, \"\u003Cunknown>\"),\n",[336,567,569],{"class":239,"line":568},7,[336,570,571],{},"))\n",[336,573,575],{"class":239,"line":574},8,[336,576,577],{},"for stat in filtered.statistics(\"lineno\")[:5]:\n",[336,579,581],{"class":239,"line":580},9,[336,582,583],{},"    print(stat)\n",[310,585,587],{"id":586},"_5-read-the-single-number-gate-with-get_traced_memory","5. Read the single-number gate with get_traced_memory",[10,589,590,591,594,595,598,599,601,602,605],{},"For a fast pass\u002Ffail, skip snapshots and read ",[14,592,593],{},"tracemalloc.get_traced_memory()",", which returns ",[14,596,597],{},"(current, peak)"," bytes since ",[14,600,366],{},". ",[14,603,604],{},"reset_peak()"," (Python 3.9+) zeroes the peak so you can measure a specific region.",[327,607,609],{"className":329,"code":608,"language":331,"meta":332,"style":332},"import tracemalloc\n\ntracemalloc.start()\nbuf = [bytes(2048) for _ in range(5_000)]\ncurrent, peak = tracemalloc.get_traced_memory()\nprint(f\"current={current\u002F1e6:.1f} MB  peak={peak\u002F1e6:.1f} MB\")\ntracemalloc.stop()\n",[14,610,611,615,619,624,629,634,639],{"__ignoreMap":332},[336,612,613],{"class":239,"line":338},[336,614,341],{},[336,616,617],{"class":239,"line":344},[336,618,348],{"emptyLinePlaceholder":347},[336,620,621],{"class":239,"line":351},[336,622,623],{},"tracemalloc.start()\n",[336,625,626],{"class":239,"line":357},[336,627,628],{},"buf = [bytes(2048) for _ in range(5_000)]\n",[336,630,631],{"class":239,"line":437},[336,632,633],{},"current, peak = tracemalloc.get_traced_memory()\n",[336,635,636],{"class":239,"line":562},[336,637,638],{},"print(f\"current={current\u002F1e6:.1f} MB  peak={peak\u002F1e6:.1f} MB\")\n",[336,640,641],{"class":239,"line":568},[336,642,643],{},"tracemalloc.stop()\n",[310,645,647],{"id":646},"_6-assert-a-memory-ceiling-in-pytest","6. Assert a memory ceiling in pytest",[10,649,650,651,654,655,660],{},"Wrap tracing in a fixture so each test starts clean and the assertion reads ",[14,652,653],{},"peak",". This is the CI counterpart to the session-fixture leaks called out in ",[656,657,659],"a",{"href":658},"\u002Fadvanced-pytest-architecture-configuration\u002F","advanced pytest architecture and configuration"," — a leaking session-scoped fixture is exactly what blows the ceiling.",[327,662,664],{"className":329,"code":663,"language":331,"meta":332,"style":332},"# conftest.py\nimport tracemalloc\nimport pytest\n\n@pytest.fixture\ndef memory_ceiling():\n    tracemalloc.start()\n    tracemalloc.reset_peak()          # Python 3.9+: ignore allocations before the test body\n    yield\n    current, peak = tracemalloc.get_traced_memory()\n    tracemalloc.stop()\n    # Surface the peak so a failing assert prints an actionable number.\n    pytest.peak_bytes = peak\n\n# test_memory.py\nimport tracemalloc\n\ndef build_report(rows):\n    return [{\"id\": r, \"blob\": bytes(1024)} for r in range(rows)]\n\ndef test_report_stays_under_5mb(memory_ceiling):\n    build_report(2_000)\n    _, peak = tracemalloc.get_traced_memory()\n    assert peak \u003C 5 * 1024 * 1024, f\"peak {peak\u002F1e6:.1f} MB exceeded 5 MB ceiling\"\n",[14,665,666,671,675,680,684,689,694,699,704,709,715,721,727,733,738,744,749,754,760,766,771,777,783,789],{"__ignoreMap":332},[336,667,668],{"class":239,"line":338},[336,669,670],{},"# conftest.py\n",[336,672,673],{"class":239,"line":344},[336,674,341],{},[336,676,677],{"class":239,"line":351},[336,678,679],{},"import pytest\n",[336,681,682],{"class":239,"line":357},[336,683,348],{"emptyLinePlaceholder":347},[336,685,686],{"class":239,"line":437},[336,687,688],{},"@pytest.fixture\n",[336,690,691],{"class":239,"line":562},[336,692,693],{},"def memory_ceiling():\n",[336,695,696],{"class":239,"line":568},[336,697,698],{},"    tracemalloc.start()\n",[336,700,701],{"class":239,"line":574},[336,702,703],{},"    tracemalloc.reset_peak()          # Python 3.9+: ignore allocations before the test body\n",[336,705,706],{"class":239,"line":580},[336,707,708],{},"    yield\n",[336,710,712],{"class":239,"line":711},10,[336,713,714],{},"    current, peak = tracemalloc.get_traced_memory()\n",[336,716,718],{"class":239,"line":717},11,[336,719,720],{},"    tracemalloc.stop()\n",[336,722,724],{"class":239,"line":723},12,[336,725,726],{},"    # Surface the peak so a failing assert prints an actionable number.\n",[336,728,730],{"class":239,"line":729},13,[336,731,732],{},"    pytest.peak_bytes = peak\n",[336,734,736],{"class":239,"line":735},14,[336,737,348],{"emptyLinePlaceholder":347},[336,739,741],{"class":239,"line":740},15,[336,742,743],{},"# test_memory.py\n",[336,745,747],{"class":239,"line":746},16,[336,748,341],{},[336,750,752],{"class":239,"line":751},17,[336,753,348],{"emptyLinePlaceholder":347},[336,755,757],{"class":239,"line":756},18,[336,758,759],{},"def build_report(rows):\n",[336,761,763],{"class":239,"line":762},19,[336,764,765],{},"    return [{\"id\": r, \"blob\": bytes(1024)} for r in range(rows)]\n",[336,767,769],{"class":239,"line":768},20,[336,770,348],{"emptyLinePlaceholder":347},[336,772,774],{"class":239,"line":773},21,[336,775,776],{},"def test_report_stays_under_5mb(memory_ceiling):\n",[336,778,780],{"class":239,"line":779},22,[336,781,782],{},"    build_report(2_000)\n",[336,784,786],{"class":239,"line":785},23,[336,787,788],{},"    _, peak = tracemalloc.get_traced_memory()\n",[336,790,792],{"class":239,"line":791},24,[336,793,794],{},"    assert peak \u003C 5 * 1024 * 1024, f\"peak {peak\u002F1e6:.1f} MB exceeded 5 MB ceiling\"\n",[10,796,797,798,801,802,806],{},"When the ceiling fails because allocations leak ",[19,799,800],{},"across"," tests rather than within one, fixture scope is usually the culprit; pin it down with the techniques in ",[656,803,805],{"href":804},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002F","mastering pytest fixtures",".",[310,808,810],{"id":809},"_7-locate-growth-by-comparing-snapshots","7. Locate growth by comparing snapshots",[10,812,813,814,816,817,819,820,822,823,827,828,806],{},"The core leak technique is diffing two snapshots. ",[14,815,118],{}," before and after a repeated operation, then ",[14,818,141],{}," to rank lines by ",[14,821,62],{},". The focused walkthroughs live in ",[656,824,826],{"href":825},"\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Ffinding-memory-leaks-with-tracemalloc-snapshots\u002F","finding memory leaks with tracemalloc snapshots"," and ",[656,829,831],{"href":830},"\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Fcomparing-tracemalloc-snapshots-to-locate-growth\u002F","comparing tracemalloc snapshots to locate growth",[327,833,835],{"className":329,"code":834,"language":331,"meta":332,"style":332},"import tracemalloc\n\ntracemalloc.start(25)\nbefore = tracemalloc.take_snapshot()\ncache = {}\nfor i in range(50_000):\n    cache[i] = bytes(64)          # an unbounded cache: the leak\nafter = tracemalloc.take_snapshot()\n\nfor stat in after.compare_to(before, \"lineno\")[:3]:\n    # size_diff is the byte growth between the two snapshots\n    print(f\"+{stat.size_diff\u002F1024:8.1f} KiB  {stat.traceback[0]}\")\n",[14,836,837,841,845,849,854,859,864,869,874,878,883,888],{"__ignoreMap":332},[336,838,839],{"class":239,"line":338},[336,840,341],{},[336,842,843],{"class":239,"line":344},[336,844,348],{"emptyLinePlaceholder":347},[336,846,847],{"class":239,"line":351},[336,848,360],{},[336,850,851],{"class":239,"line":357},[336,852,853],{},"before = tracemalloc.take_snapshot()\n",[336,855,856],{"class":239,"line":437},[336,857,858],{},"cache = {}\n",[336,860,861],{"class":239,"line":562},[336,862,863],{},"for i in range(50_000):\n",[336,865,866],{"class":239,"line":568},[336,867,868],{},"    cache[i] = bytes(64)          # an unbounded cache: the leak\n",[336,870,871],{"class":239,"line":574},[336,872,873],{},"after = tracemalloc.take_snapshot()\n",[336,875,876],{"class":239,"line":580},[336,877,348],{"emptyLinePlaceholder":347},[336,879,880],{"class":239,"line":711},[336,881,882],{},"for stat in after.compare_to(before, \"lineno\")[:3]:\n",[336,884,885],{"class":239,"line":717},[336,886,887],{},"    # size_diff is the byte growth between the two snapshots\n",[336,889,890],{"class":239,"line":723},[336,891,892],{},"    print(f\"+{stat.size_diff\u002F1024:8.1f} KiB  {stat.traceback[0]}\")\n",[35,894,896],{"id":895},"verification","Verification",[40,898,899,909,915,922],{},[43,900,901,902,905,906,806],{},"Confirm tracing is live before measuring: ",[14,903,904],{},"tracemalloc.is_tracing()"," must return ",[14,907,908],{},"True",[43,910,911,912,914],{},"Sanity-check ",[14,913,149],{}," peak against a known allocation — allocate a 10 MB list and verify the peak rises by roughly that much.",[43,916,917,918,921],{},"Run the pytest gate with ",[14,919,920],{},"-q"," and deliberately bump the workload above the ceiling once; the assertion message must print the real peak in MB.",[43,923,924,925,928,929,932],{},"Cross-check the suspected leaking line by printing ",[14,926,927],{},"stat.count"," (block count) alongside ",[14,930,931],{},"stat.size"," — a line whose count grows unboundedly across iterations is a leak, not a one-time buffer.",[35,934,936],{"id":935},"troubleshooting","Troubleshooting",[938,939,940,956],"table",{},[941,942,943],"thead",{},[944,945,946,950,953],"tr",{},[947,948,949],"th",{},"Symptom",[947,951,952],{},"Root cause",[947,954,955],{},"Fix",[957,958,959,983,1004,1023,1042,1057],"tbody",{},[944,960,961,967,975],{},[962,963,964],"td",{},[14,965,966],{},"RuntimeError: the tracemalloc module must be tracing memory",[962,968,969,970,972,973],{},"Called ",[14,971,118],{}," before ",[14,974,366],{},[962,976,977,978,980,981],{},"Call ",[14,979,73],{}," first, or set ",[14,982,380],{},[944,984,985,991,994],{},[962,986,987,988],{},"Top stats point only at ",[14,989,990],{},"\u003Cfrozen importlib._bootstrap>",[962,992,993],{},"Import machinery dominates unfiltered snapshots",[962,995,996,997,999,1000,1003],{},"Apply ",[14,998,134],{}," with ",[14,1001,1002],{},"Filter(False, ...)"," for importlib",[944,1005,1006,1012,1017],{},[962,1007,1008,1011],{},[14,1009,1010],{},"traceback"," only shows one frame",[962,1013,1014,1016],{},[14,1015,94],{}," too low (default 1)",[962,1018,1019,1020],{},"Restart with ",[14,1021,1022],{},"tracemalloc.start(25)",[944,1024,1025,1028,1031],{},[962,1026,1027],{},"tracemalloc number much lower than RSS",[962,1029,1030],{},"C-extension allocations are invisible to it",[962,1032,1033,1034,1037,1038,1041],{},"Cross-check with ",[14,1035,1036],{},"psutil","\u002F",[14,1039,1040],{},"memray"," for non-Python memory",[944,1043,1044,1047,1052],{},[962,1045,1046],{},"Peak includes setup you do not care about",[962,1048,1049,1050],{},"Peak accumulates since ",[14,1051,366],{},[962,1053,977,1054,1056],{},[14,1055,604],{}," (3.9+) right before the region",[944,1058,1059,1062,1065],{},[962,1060,1061],{},"Snapshot diff shows growth that is actually a cache warm-up",[962,1063,1064],{},"First snapshot taken too early",[962,1066,1067],{},"Take the baseline after warm-up, then loop the operation",[35,1069,1071],{"id":1070},"frequently-asked-questions","Frequently Asked Questions",[10,1073,1074,1077,1079,1080,1083],{},[46,1075,1076],{},"What does the nframe argument to tracemalloc.start() control?",[14,1078,94],{}," sets how many stack frames tracemalloc records for each allocation. With ",[14,1081,1082],{},"nframe=1"," (the default) you only get the line that allocated; a higher value lets you group by full traceback to see the call path that led to the allocation, at the cost of more memory and overhead.",[10,1085,1086,1089,1091,1092,1094],{},[46,1087,1088],{},"Why does tracemalloc report less memory than the operating system?",[14,1090,29],{}," only tracks allocations made through Python's memory allocators after ",[14,1093,366],{}," was called. Memory allocated by C extensions outside pymalloc, allocations made before tracing started, and interpreter overhead are invisible to it, so RSS is always larger.",[10,1096,1097,1100,1101,1103,1104,1106],{},[46,1098,1099],{},"How do I assert a memory ceiling in a pytest test?","\nCall ",[14,1102,73],{}," in a fixture, run the code under test, then read ",[14,1105,593],{}," which returns current and peak bytes. Assert the peak against a threshold and stop tracing in teardown so the next test starts clean.",[10,1108,1109,1112,1115,1116,1119],{},[46,1110,1111],{},"What is the difference between grouping statistics by lineno and by traceback?",[14,1113,1114],{},"group_by='lineno'"," aggregates all allocations on the same source line into one entry, regardless of how that line was reached. ",[14,1117,1118],{},"group_by='traceback'"," keeps each distinct call path separate, which is essential when the same helper allocates on behalf of many callers.",[35,1121,1123],{"id":1122},"related-guides","Related guides",[40,1125,1126,1131,1136,1144],{},[43,1127,1128,1129,806],{},"For the full leak-hunting recipe, follow ",[656,1130,826],{"href":825},[43,1132,1133,1134,806],{},"To rank which lines grew between two points in time, use ",[656,1135,831],{"href":830},[43,1137,1138,1139,1143],{},"When the leak is CPU-bound work rather than retained bytes, switch to ",[656,1140,1142],{"href":1141},"\u002Fsystematic-debugging-performance-profiling\u002Finteractive-debugging-with-pdb-and-ipdb\u002F","interactive debugging with pdb and ipdb"," to step through the allocating path.",[43,1145,1146,1147,806],{},"Memory that grows only under concurrency often points at retained tasks; see ",[656,1148,1150],{"href":1149},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002F","debugging async code and event loops",[10,1152,1153,1154],{},"← Back to ",[656,1155,1157],{"href":1156},"\u002Fsystematic-debugging-performance-profiling\u002F","Systematic Debugging & Performance Profiling",[1159,1160,1161],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":332,"searchDepth":344,"depth":344,"links":1163},[1164,1165,1166,1175,1176,1177,1178],{"id":37,"depth":344,"text":38},{"id":98,"depth":344,"text":99},{"id":307,"depth":344,"text":308,"children":1167},[1168,1169,1170,1171,1172,1173,1174],{"id":312,"depth":351,"text":313},{"id":402,"depth":351,"text":403},{"id":443,"depth":351,"text":444},{"id":519,"depth":351,"text":520},{"id":586,"depth":351,"text":587},{"id":646,"depth":351,"text":647},{"id":809,"depth":351,"text":810},{"id":895,"depth":344,"text":896},{"id":935,"depth":344,"text":936},{"id":1070,"depth":344,"text":1071},{"id":1122,"depth":344,"text":1123},"Profile Python memory with tracemalloc: start tracing with nframe, take snapshots, group statistics by lineno and traceback, apply filters, and assert ceilings in pytest.","md",{"slug":1182,"type":1183,"breadcrumb":29,"datePublished":1184,"dateModified":1184,"faq":1185,"howto":1194},"memory-profiling-with-tracemalloc","cluster","2026-06-18",[1186,1188,1190,1192],{"q":1076,"a":1187},"nframe sets how many stack frames tracemalloc records for each allocation. With nframe=1 (the default) you only get the line that allocated; a higher value lets you group by full traceback to see the call path that led to the allocation, at the cost of more memory and overhead.",{"q":1088,"a":1189},"tracemalloc only tracks allocations made through Python's memory allocators after start() was called. Memory allocated by C extensions outside pymalloc, allocations made before tracing started, and interpreter overhead are invisible to it, so RSS is always larger.",{"q":1099,"a":1191},"Call tracemalloc.start() in a fixture, run the code under test, then read tracemalloc.get_traced_memory() which returns current and peak bytes. Assert the peak against a threshold and stop tracing in teardown so the next test starts clean.",{"q":1111,"a":1193},"group_by='lineno' aggregates all allocations on the same source line into one entry, regardless of how that line was reached. group_by='traceback' keeps each distinct call path separate, which is essential when the same helper allocates on behalf of many callers.",{"name":1195,"description":1196,"steps":1197},"How to profile memory with tracemalloc","Start tracing, capture a snapshot, group and filter the statistics, and assert a memory ceiling in a test.",[1198,1201,1204,1207],{"name":1199,"text":1200},"Start tracing with enough frames","Call tracemalloc.start(nframe) before the code under test, choosing nframe high enough to capture the allocating call path.",{"name":1202,"text":1203},"Take a snapshot","Call tracemalloc.take_snapshot() at the point of interest to freeze the current set of tracked allocations.",{"name":1205,"text":1206},"Group and filter the statistics","Call snapshot.statistics('lineno') or 'traceback', and apply snapshot.filter_traces to drop noise from the standard library and tracemalloc itself.",{"name":1208,"text":1209},"Assert a ceiling and stop tracing","Read get_traced_memory() for current and peak bytes, assert peak against a threshold, then call tracemalloc.stop().","\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc",{"title":5,"description":1179},"systematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Findex","Yi9_e-v_LcyfDMBFbNDW-j18HXgrhUpSgqHdLWWa1TA",1781793487031]