[{"data":1,"prerenderedAt":869},["ShallowReactive",2],{"page-\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Fprofiling-memory-with-memray\u002F":3},{"id":4,"title":5,"body":6,"description":833,"extension":834,"meta":835,"navigation":108,"path":865,"seo":866,"stem":867,"__hash__":868},"content\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Fprofiling-memory-with-memray\u002Findex.md","Profiling Memory with memray",{"type":7,"value":8,"toc":822},"minimark",[9,17,20,25,48,52,256,316,430,434,453,456,462,466,469,480,541,545,555,574,585,592,657,661,672,695,699,743,747,753,762,780,784,813,818],[10,11,12,16],"p",{},[13,14,15],"code",{},"tracemalloc"," answers \"which Python lines allocated the memory that is live right now?\" and it is built in, which makes it the right first tool. It has limits that matter for real applications. It only sees allocations that go through Python's allocator, so a NumPy array's buffer, a Pandas block, an image decoded by Pillow or a protobuf message built in C is invisible or reported as a tiny wrapper object. It runs inside the process, so snapshots must be taken from code. And it shows lines, not the call stacks that led to them.",[10,18,19],{},"memray, from Bloomberg, fills those gaps. It intercepts every allocation — Python and native — writes them with full stacks to a compact file, and produces reports afterwards: a flame graph of memory at peak, a table of top allocators, a summary, a leak report, and a live terminal view for long-running processes. Its overhead is low enough to run realistic workloads, and a pytest plugin turns its measurements into assertions.",[21,22,24],"h2",{"id":23},"prerequisites","Prerequisites",[26,27,28,40],"ul",{},[29,30,31,32,35,36,39],"li",{},"Linux or macOS, Python 3.8 or later, ",[13,33,34],{},"memray >= 1.12",", optionally ",[13,37,38],{},"pytest-memray >= 1.6",".",[29,41,42,43,39],{},"Background from ",[44,45,47],"a",{"href":46},"\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002F","Memory profiling with tracemalloc",[21,49,51],{"id":50},"solution","Solution",[53,54,59],"pre",{"className":55,"code":56,"language":57,"meta":58,"style":58},"language-bash shiki shiki-themes github-light github-dark","# Record a run: every allocation, with Python stacks.\nmemray run -o build_report.bin -m app.reports build --month 2026-08\n\n# Flame graph of memory held at peak usage.\nmemray flamegraph build_report.bin            # → memray-flamegraph-build_report.html\n\n# Quick terminal views.\nmemray summary build_report.bin\nmemray stats build_report.bin\n\n# Include native frames when C extensions allocate.\nmemray run --native -o native.bin -m app.reports build --month 2026-08\n\n# Leak hunting: disable pymalloc so every free is visible, then view leaks.\nPYTHONMALLOC=malloc memray run -o leaks.bin -m app.worker --iterations 500\nmemray flamegraph --leaks leaks.bin\n","bash","",[13,60,61,70,103,110,116,129,134,140,151,161,166,172,197,202,208,243],{"__ignoreMap":58},[62,63,66],"span",{"class":64,"line":65},"line",1,[62,67,69],{"class":68},"sJ8bj","# Record a run: every allocation, with Python stacks.\n",[62,71,73,77,81,85,88,91,94,97,100],{"class":64,"line":72},2,[62,74,76],{"class":75},"sScJk","memray",[62,78,80],{"class":79},"sZZnC"," run",[62,82,84],{"class":83},"sj4cs"," -o",[62,86,87],{"class":79}," build_report.bin",[62,89,90],{"class":83}," -m",[62,92,93],{"class":79}," app.reports",[62,95,96],{"class":79}," build",[62,98,99],{"class":83}," --month",[62,101,102],{"class":79}," 2026-08\n",[62,104,106],{"class":64,"line":105},3,[62,107,109],{"emptyLinePlaceholder":108},true,"\n",[62,111,113],{"class":64,"line":112},4,[62,114,115],{"class":68},"# Flame graph of memory held at peak usage.\n",[62,117,119,121,124,126],{"class":64,"line":118},5,[62,120,76],{"class":75},[62,122,123],{"class":79}," flamegraph",[62,125,87],{"class":79},[62,127,128],{"class":68},"            # → memray-flamegraph-build_report.html\n",[62,130,132],{"class":64,"line":131},6,[62,133,109],{"emptyLinePlaceholder":108},[62,135,137],{"class":64,"line":136},7,[62,138,139],{"class":68},"# Quick terminal views.\n",[62,141,143,145,148],{"class":64,"line":142},8,[62,144,76],{"class":75},[62,146,147],{"class":79}," summary",[62,149,150],{"class":79}," build_report.bin\n",[62,152,154,156,159],{"class":64,"line":153},9,[62,155,76],{"class":75},[62,157,158],{"class":79}," stats",[62,160,150],{"class":79},[62,162,164],{"class":64,"line":163},10,[62,165,109],{"emptyLinePlaceholder":108},[62,167,169],{"class":64,"line":168},11,[62,170,171],{"class":68},"# Include native frames when C extensions allocate.\n",[62,173,175,177,179,182,184,187,189,191,193,195],{"class":64,"line":174},12,[62,176,76],{"class":75},[62,178,80],{"class":79},[62,180,181],{"class":83}," --native",[62,183,84],{"class":83},[62,185,186],{"class":79}," native.bin",[62,188,90],{"class":83},[62,190,93],{"class":79},[62,192,96],{"class":79},[62,194,99],{"class":83},[62,196,102],{"class":79},[62,198,200],{"class":64,"line":199},13,[62,201,109],{"emptyLinePlaceholder":108},[62,203,205],{"class":64,"line":204},14,[62,206,207],{"class":68},"# Leak hunting: disable pymalloc so every free is visible, then view leaks.\n",[62,209,211,215,219,222,225,227,229,232,234,237,240],{"class":64,"line":210},15,[62,212,214],{"class":213},"sVt8B","PYTHONMALLOC",[62,216,218],{"class":217},"szBVR","=",[62,220,221],{"class":79},"malloc",[62,223,224],{"class":75}," memray",[62,226,80],{"class":79},[62,228,84],{"class":83},[62,230,231],{"class":79}," leaks.bin",[62,233,90],{"class":83},[62,235,236],{"class":79}," app.worker",[62,238,239],{"class":83}," --iterations",[62,241,242],{"class":83}," 500\n",[62,244,246,248,250,253],{"class":64,"line":245},16,[62,247,76],{"class":75},[62,249,123],{"class":79},[62,251,252],{"class":83}," --leaks",[62,254,255],{"class":79}," leaks.bin\n",[53,257,261],{"className":258,"code":259,"language":260,"meta":58,"style":58},"language-python shiki shiki-themes github-light github-dark","# test_reports_memory.py — budgets enforced in CI with pytest --memray\nimport pytest\n\n@pytest.mark.limit_memory(\"150 MB\")\ndef test_monthly_report_fits_in_budget(sample_month):\n    build_report(sample_month)\n\n@pytest.mark.limit_leaks(\"1 MB\")\ndef test_worker_iteration_does_not_leak(worker):\n    for _ in range(100):\n        worker.process_one()\n","python",[13,262,263,268,273,277,282,287,292,296,301,306,311],{"__ignoreMap":58},[62,264,265],{"class":64,"line":65},[62,266,267],{},"# test_reports_memory.py — budgets enforced in CI with pytest --memray\n",[62,269,270],{"class":64,"line":72},[62,271,272],{},"import pytest\n",[62,274,275],{"class":64,"line":105},[62,276,109],{"emptyLinePlaceholder":108},[62,278,279],{"class":64,"line":112},[62,280,281],{},"@pytest.mark.limit_memory(\"150 MB\")\n",[62,283,284],{"class":64,"line":118},[62,285,286],{},"def test_monthly_report_fits_in_budget(sample_month):\n",[62,288,289],{"class":64,"line":131},[62,290,291],{},"    build_report(sample_month)\n",[62,293,294],{"class":64,"line":136},[62,295,109],{"emptyLinePlaceholder":108},[62,297,298],{"class":64,"line":142},[62,299,300],{},"@pytest.mark.limit_leaks(\"1 MB\")\n",[62,302,303],{"class":64,"line":153},[62,304,305],{},"def test_worker_iteration_does_not_leak(worker):\n",[62,307,308],{"class":64,"line":163},[62,309,310],{},"    for _ in range(100):\n",[62,312,313],{"class":64,"line":168},[62,314,315],{},"        worker.process_one()\n",[317,318,321,426],"figure",{"className":319},[320],"diagram",[322,323,330,331,330,335,330,339,330,347,330,357,330,366,330,371,330,378,330,383,330,389,330,393,330,396,330,400,330,405,330,411,330,417,330,422],"svg",{"viewBox":324,"role":325,"ariaLabelledBy":326,"xmlns":329},"0 0 800 256","img",[327,328],"mr-t","mr-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[332,333,334],"title",{"id":327},"Reading a memray peak-memory flame graph",[336,337,338],"desc",{"id":328},"A flame graph with the root at the top and callees below. The width of each bar is bytes held at peak memory, not time. Under the report build function, a wide bar for load_rows into pandas read_csv dominates, while a narrow bar for rendering uses little memory. The widest leaf is where to optimise.",[340,341],"rect",{"x":342,"y":342,"width":343,"height":344,"rx":345,"fill":346},"0","800","256","14","#fffdf8",[348,349,356],"text",{"x":350,"y":351,"textAnchor":352,"fontSize":353,"fontWeight":354,"fill":355},"400","28","middle","15.5","700","#3d405b","Width is bytes held at peak",[340,358],{"x":359,"y":360,"width":361,"height":362,"rx":363,"fill":364,"stroke":355,"strokeWidth":365},"40","50","720","30","5","#f4f1de","1.2",[348,367,370],{"x":350,"y":368,"textAnchor":352,"fontSize":369,"fill":355},"70","11","build_report — 1.9 GB at peak",[340,372],{"x":359,"y":373,"width":374,"height":362,"rx":363,"fill":375,"stroke":376,"strokeWidth":377},"86","560","#fbe9e3","#e07a5f","1.4",[348,379,382],{"x":380,"y":381,"textAnchor":352,"fontSize":369,"fill":355},"320","106","load_rows — 1.5 GB",[340,384],{"x":385,"y":373,"width":386,"height":362,"rx":363,"fill":387,"stroke":388,"strokeWidth":377},"606","154","#e6f0ea","#81b29a",[348,390,392],{"x":391,"y":381,"textAnchor":352,"fontSize":369,"fill":355},"683","aggregate — 0.4 GB",[340,394],{"x":359,"y":395,"width":374,"height":362,"rx":363,"fill":375,"stroke":376,"strokeWidth":377},"122",[348,397,399],{"x":380,"y":398,"textAnchor":352,"fontSize":369,"fill":355},"142","pandas.read_csv",[340,401],{"x":359,"y":402,"width":403,"height":362,"rx":363,"fill":375,"stroke":376,"strokeWidth":404},"158","420","1.8",[348,406,410],{"x":407,"y":408,"textAnchor":352,"fontSize":369,"fontWeight":354,"fill":409},"250","178","#8f3d22","object-dtype string columns",[340,412],{"x":413,"y":402,"width":414,"height":362,"rx":363,"fill":415,"stroke":416,"strokeWidth":377},"466","134","#f7f0da","#f2cc8f",[348,418,421],{"x":419,"y":408,"textAnchor":352,"fontSize":420,"fill":355},"533","10.5","float blocks",[348,423,425],{"x":350,"y":424,"textAnchor":352,"fontSize":369,"fill":355},"222","Fix the widest leaf first: categorical or pyarrow string dtypes.",[427,428,429],"figcaption",{},"The widest frame at the bottom of a stack is the allocation to attack; narrow frames are not worth optimising yet.",[21,431,433],{"id":432},"why-this-works","Why this works",[10,435,436,437,439,440,439,443,439,446,439,449,452],{},"memray installs hooks on the allocator functions — ",[13,438,221],{},", ",[13,441,442],{},"calloc",[13,444,445],{},"realloc",[13,447,448],{},"free",[13,450,451],{},"mmap"," and Python's own allocator domains — so every allocation in the process passes through it, regardless of which library made it. For each one it records the size and a reference to the current stack, deduplicated so that repeated stacks cost almost nothing, and streams the records to the output file. Reports are computed afterwards from that file, which is why generating a different view never requires re-running the program.",[10,454,455],{},"The default flame graph reconstructs the heap at the moment of highest total usage and attributes each live byte to the stack that allocated it. That is usually the question that matters for out-of-memory crashes and container limits: not \"what allocated the most over time\" but \"what was holding memory when we were at our worst\". Frames are merged by stack, so width is proportional to bytes and the widest leaf is the biggest single contributor.",[10,457,458,461],{},[13,459,460],{},"--native"," adds C and C++ frames to the stacks, resolved from debug symbols. Without it, an allocation made deep inside NumPy is attributed to the Python line that called into NumPy, which is often enough. With it, you can distinguish, say, a temporary array created by broadcasting from the output array, which matters when choosing between in-place operations and chunking.",[21,463,465],{"id":464},"leaks-versus-peak","Leaks versus peak",[10,467,468],{},"Peak memory and leaks are different problems with different reports. A report builder that loads a whole month into memory has a high peak and no leak: memory returns after the call. A worker that caches a reference to every processed message has a low peak per iteration and a steady leak. The first is fixed by processing in chunks or using more compact types; the second by finding the reference that keeps objects alive.",[10,470,471,472,475,476,479],{},"For leaks, record a run long enough for the leak to dominate — hundreds of iterations, not one — and view it with ",[13,473,474],{},"--leaks",", which shows memory allocated and never freed by the end of the run. Set ",[13,477,478],{},"PYTHONMALLOC=malloc"," while recording: Python's small-object allocator keeps freed memory in arenas for reuse, and memray would otherwise see those arenas as never freed. The leak flame graph then points at the stacks that allocated the objects still alive at exit.",[317,481,483,538],{"className":482},[320],[322,484,330,489,330,492,330,495,330,498,330,501,330,508,330,514,330,520,330,525,330,528,330,532,330,535],{"viewBox":485,"role":325,"ariaLabelledBy":486,"xmlns":329},"0 0 800 236",[487,488],"mrl-t","mrl-d",[332,490,491],{"id":487},"Peak memory versus a leak",[336,493,494],{"id":488},"Two memory-over-time curves. The peak profile rises sharply during a load, then returns to baseline, and is analysed with the default peak flame graph. The leak profile climbs steadily across iterations and never returns, and is analysed with the leaks flame graph recorded with PYTHONMALLOC set to malloc.",[340,496],{"x":342,"y":342,"width":343,"height":497,"rx":345,"fill":346},"236",[348,499,500],{"x":350,"y":351,"textAnchor":352,"fontSize":353,"fontWeight":354,"fill":355},"Two shapes, two reports",[340,502],{"x":503,"y":360,"width":504,"height":505,"rx":506,"fill":415,"stroke":416,"strokeWidth":507},"26","360","164","12","2",[348,509,513],{"x":510,"y":511,"textAnchor":352,"fontSize":512,"fontWeight":354,"fill":355},"206","74","12.5","peak",[515,516],"path",{"d":517,"fill":518,"stroke":376,"strokeWidth":519},"M50 180 L130 176 L160 90 L220 86 L250 176 L360 178","none","2.4",[348,521,524],{"x":510,"y":522,"textAnchor":352,"fontSize":420,"fill":523},"204","#8a5a00","memray flamegraph out.bin",[340,526],{"x":527,"y":360,"width":504,"height":505,"rx":506,"fill":375,"stroke":376,"strokeWidth":507},"414",[348,529,531],{"x":530,"y":511,"textAnchor":352,"fontSize":512,"fontWeight":354,"fill":355},"594","leak",[515,533],{"d":534,"fill":518,"stroke":376,"strokeWidth":519},"M438 180 L480 170 L520 158 L560 146 L600 134 L640 122 L680 110 L740 96",[348,536,537],{"x":530,"y":522,"textAnchor":352,"fontSize":420,"fill":409},"flamegraph --leaks (PYTHONMALLOC=malloc)",[427,539,540],{},"Decide which shape you have before choosing a report; the default flame graph will not find a slow leak.",[21,542,544],{"id":543},"a-worked-investigation-the-report-that-needed-2-gb","A worked investigation: the report that needed 2 GB",[10,546,547,548,550,551,554],{},"The flame graph above comes from a common situation. A monthly report job started failing with out-of-memory kills after the container limit was lowered to 1.5 GB. ",[13,549,15],{}," pointed vaguely at ",[13,552,553],{},"read_csv"," and reported a few hundred megabytes, far less than the container was using, because most of the memory was held in NumPy and pandas buffers outside Python's allocator.",[10,556,557,558,561,562,565,566,569,570,573],{},"A single ",[13,559,560],{},"memray run"," followed by ",[13,563,564],{},"memray flamegraph"," showed the full picture. At peak, 1.5 GB of 1.9 GB sat under ",[13,567,568],{},"load_rows",", and within that, the widest leaf was the construction of object-dtype columns: every string cell was a separate Python ",[13,571,572],{},"str"," object with its own header, plus an 8-byte pointer in the column array. The numeric columns, by contrast, were compact float blocks and barely registered.",[10,575,576,577,580,581,584],{},"Two changes followed directly from the graph. Low-cardinality string columns such as country and status were loaded as ",[13,578,579],{},"category",", which stores each distinct value once and keeps small integer codes per row. High-cardinality columns such as customer names switched to the ",[13,582,583],{},"string[pyarrow]"," dtype, which stores the text in one contiguous Arrow buffer. A second memray run confirmed the effect: peak memory fell to about 700 MB, and the widest frame moved to the aggregation step, where it belonged.",[10,586,587,588,591],{},"The final step was the budget test in the solution above. With ",[13,589,590],{},"limit_memory(\"150 MB\")"," on a representative sample month, any future change that reintroduced object columns — a new field loaded without a dtype, say — fails in CI with a report naming the allocating stack, long before it reaches the production container.",[317,593,595,654],{"className":594},[320],[322,596,330,601,330,604,330,607,330,610,330,613,330,617,330,623,330,628,330,633,330,637,330,642,330,650],{"viewBox":597,"role":325,"ariaLabelledBy":598,"xmlns":329},"0 0 800 226",[599,600],"mrw-t","mrw-d",[332,602,603],{"id":599},"Peak memory before and after the fix",[336,605,606],{"id":600},"Two horizontal bars compare peak memory. Before the fix, peak was 1.9 gigabytes, above the 1.5 gigabyte container limit, dominated by object-dtype string columns. After switching to categorical and pyarrow string dtypes, peak fell to about 0.7 gigabytes, comfortably under the limit.",[340,608],{"x":342,"y":342,"width":343,"height":609,"rx":345,"fill":346},"226",[348,611,612],{"x":350,"y":351,"textAnchor":352,"fontSize":353,"fontWeight":354,"fill":355},"One flame graph, one dtype change",[348,614,616],{"x":359,"y":615,"fontSize":506,"fontWeight":354,"fill":409},"84","before",[340,618],{"x":619,"y":620,"width":621,"height":351,"rx":622,"fill":375,"stroke":376,"strokeWidth":404},"130","66","608","6",[348,624,627],{"x":625,"y":626,"textAnchor":352,"fontSize":369,"fill":355},"434","85","1.9 GB — object string columns dominate",[348,629,632],{"x":359,"y":630,"fontSize":506,"fontWeight":354,"fill":631},"144","#2a5f49","after",[340,634],{"x":619,"y":635,"width":636,"height":351,"rx":622,"fill":387,"stroke":388,"strokeWidth":404},"126","224",[348,638,641],{"x":639,"y":640,"textAnchor":352,"fontSize":369,"fill":355},"242","145","0.7 GB",[64,643],{"x1":644,"y1":645,"x2":644,"y2":646,"stroke":355,"strokeWidth":647,"strokeDashArray":648},"610","52","176","1.6",[363,649],"4",[348,651,653],{"x":644,"y":652,"textAnchor":352,"fontSize":369,"fill":355},"194","container limit 1.5 GB",[427,655,656],{},"tracemalloc could not see the buffers that mattered; memray's native tracking made the fix obvious.",[21,658,660],{"id":659},"watching-a-live-process","Watching a live process",[10,662,663,664,667,668,671],{},"Some memory problems only appear in long-running services, after hours of real traffic. Recording the whole run to a file is possible, but memray also offers a live mode. ",[13,665,666],{},"memray run --live-remote --live-port 12345 -m app.server"," starts the service with tracking enabled and waits for a viewer; ",[13,669,670],{},"memray live 12345"," in another terminal attaches a text interface that shows the current heap, broken down by allocating location and refreshing as the process runs. Sorting by total memory and watching which rows grow over several minutes is often enough to spot a cache or buffer that never shrinks.",[10,673,674,675,678,679,682,683,686,687,690,691,694],{},"For a process that is already running without memray, ",[13,676,677],{},"memray attach \u003Cpid>"," injects the tracker through a debugger and starts recording from that moment. It needs ",[13,680,681],{},"gdb"," or ",[13,684,685],{},"lldb"," and permission to ptrace the process — in containers, that usually means adding the ",[13,688,689],{},"SYS_PTRACE"," capability — but it avoids restarting a service that took hours to reach the problematic state. The resulting capture shows allocations since attach, which, for a leak, is exactly the part that matters. Detach with ",[13,692,693],{},"memray detach"," once enough has been recorded, and the service carries on without tracking overhead, so the technique is safe to use on a staging system under real load.",[21,696,698],{"id":697},"edge-cases-and-failure-modes","Edge cases and failure modes",[26,700,701,712,721,731,737],{},[29,702,703,707,708,711],{},[704,705,706],"strong",{},"Huge output files."," Long runs with many allocations produce gigabyte-sized captures. Use ",[13,709,710],{},"--aggregate"," (memray 1.11+) to store only what the peak and leak reports need.",[29,713,714,717,718,39],{},[704,715,716],{},"Missing native symbols."," Native frames appear as addresses without debug information. Install debug symbol packages or build extensions with ",[13,719,720],{},"-g",[29,722,723,726,727,730],{},[704,724,725],{},"Forked workers."," Child processes are not tracked by default. Use ",[13,728,729],{},"--follow-fork"," for multiprocessing and pre-fork servers.",[29,732,733,736],{},[704,734,735],{},"macOS limitations."," Native tracking and some allocators behave differently on macOS; do deep investigations on Linux.",[29,738,739,742],{},[704,740,741],{},"Overhead on allocation-heavy code."," Programs making millions of small allocations per second slow noticeably. Profile a representative slice rather than the full job.",[21,744,746],{"id":745},"frequently-asked-questions","Frequently Asked Questions",[10,748,749,752],{},[704,750,751],{},"How is memray different from tracemalloc?","\ntracemalloc tracks only allocations made through Python's allocator and runs inside the process. memray intercepts native allocations too, including those from C extensions like NumPy, records to a file with low overhead, and produces flame graphs and reports afterwards.",[10,754,755,758,759,761],{},[704,756,757],{},"What does the memray flame graph show?","\nBy default it shows memory held at the moment of peak usage, attributed to the stacks that allocated it. Width is bytes, not time. With ",[13,760,474],{}," it shows memory allocated and never freed by the end of the run.",[10,763,764,767,768,771,772,775,776,779],{},[704,765,766],{},"Can I use memray in pytest?","\nYes. The pytest-memray plugin runs tests under memray with ",[13,769,770],{},"--memray",", reports the biggest allocators per test, and supports ",[13,773,774],{},"limit_memory"," and ",[13,777,778],{},"limit_leaks"," markers that fail tests exceeding a budget.",[21,781,783],{"id":782},"related","Related",[26,785,786,792,799,806],{},[29,787,788,791],{},[44,789,790],{"href":46},"Memory Profiling with tracemalloc"," — the built-in tool and its model.",[29,793,794,798],{},[44,795,797],{"href":796},"\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Ffinding-memory-leaks-with-tracemalloc-snapshots\u002F","Finding Memory Leaks with tracemalloc Snapshots"," — snapshot diffs.",[29,800,801,805],{},[44,802,804],{"href":803},"\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Fcatching-per-test-memory-growth-in-pytest\u002F","Catching Per-Test Memory Growth in pytest"," — budgets across a suite.",[29,807,808,812],{},[44,809,811],{"href":810},"\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Ffinding-reference-cycles-with-gc-and-objgraph\u002F","Finding Reference Cycles with gc and objgraph"," — what holds leaked objects alive.",[10,814,815,816],{},"← Back to ",[44,817,790],{"href":46},[819,820,821],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .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);}",{"title":58,"searchDepth":72,"depth":72,"links":823},[824,825,826,827,828,829,830,831,832],{"id":23,"depth":72,"text":24},{"id":50,"depth":72,"text":51},{"id":432,"depth":72,"text":433},{"id":464,"depth":72,"text":465},{"id":543,"depth":72,"text":544},{"id":659,"depth":72,"text":660},{"id":697,"depth":72,"text":698},{"id":745,"depth":72,"text":746},{"id":782,"depth":72,"text":783},"Find where Python programs allocate memory with memray: recording runs, flame graphs of peak memory, native allocations from C extensions, leak mode, live mode and the pytest-memray plugin.","md",{"slug":836,"type":837,"breadcrumb":76,"datePublished":838,"dateModified":838,"faq":839,"howto":846},"profiling-memory-with-memray","article","2026-09-18",[840,842,844],{"q":751,"a":841},"tracemalloc tracks only allocations made through Python's allocator and runs inside the process. memray intercepts native allocations too, including those from C extensions like NumPy, records to a file with low overhead, and produces flame graphs and reports afterwards.",{"q":757,"a":843},"By default it shows memory held at the moment of peak usage, attributed to the stacks that allocated it. Width is bytes, not time. With --leaks it shows memory allocated and never freed by the end of the run.",{"q":766,"a":845},"Yes. The pytest-memray plugin runs tests under memray with --memray, reports the biggest allocators per test, and supports limit_memory and limit_leaks markers that fail tests exceeding a budget.",{"name":847,"description":848,"steps":849},"How to profile memory with memray","Record a run, open the peak-memory flame graph, include native frames when extensions are involved, and enforce budgets in tests.",[850,853,856,859,862],{"name":851,"text":852},"Record a run","Run memray run -o out.bin script.py to capture every allocation with stacks.",{"name":854,"text":855},"Open the flame graph","Generate memray flamegraph out.bin and read the widest stacks at peak memory.",{"name":857,"text":858},"Include native frames","Record with --native when C extensions allocate, so their stacks appear.",{"name":860,"text":861},"Check for leaks","Use --leaks on the flame graph with PYTHONMALLOC=malloc to see memory never freed.",{"name":863,"text":864},"Budget memory in tests","Mark tests with pytest.mark.limit_memory and run pytest --memray.","\u002Fsystematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Fprofiling-memory-with-memray",{"title":5,"description":833},"systematic-debugging-performance-profiling\u002Fmemory-profiling-with-tracemalloc\u002Fprofiling-memory-with-memray\u002Findex","hDeRCki7skR7ewN3FAbz8HnBBAeS5xeK2UCPmp_TQTI",1789718767669]