[{"data":1,"prerenderedAt":491},["ShallowReactive",2],{"page-\u002Fsystematic-debugging-performance-profiling\u002Fcpu-profiling-with-cprofile-and-py-spy\u002Fprofiling-a-running-process-with-py-spy\u002F":3},{"id":4,"title":5,"body":6,"description":453,"extension":454,"meta":455,"navigation":486,"path":487,"seo":488,"stem":489,"__hash__":490},"content\u002Fsystematic-debugging-performance-profiling\u002Fcpu-profiling-with-cprofile-and-py-spy\u002Fprofiling-a-running-process-with-py-spy\u002Findex.md","Profiling a Running Process with py-spy",{"type":7,"value":8,"toc":446},"minimark",[9,22,27,80,84,87,135,141,166,172,215,221,242,248,293,297,318,322,391,395,401,427,435,442],[10,11,12,13,17,18,21],"p",{},"You have a Python worker pegging a CPU in production and restarting it to add instrumentation is not an option — you need to see what it is doing right now, from outside, without touching its code. ",[14,15,16],"code",{},"py-spy"," does exactly this: it attaches to a running interpreter by PID, reads the call stacks through OS debugging facilities, and renders them as a live view, a flame graph, or a one-shot dump. This guide covers the three subcommands and the Linux ",[14,19,20],{},"ptrace_scope"," permission wall that blocks the first attempt on most hosts.",[23,24,26],"h2",{"id":25},"prerequisites","Prerequisites",[28,29,30,57,60,74],"ul",{},[31,32,33,36,37,40,41,44,45,48,49,52,53,56],"li",{},[14,34,35],{},"py-spy >= 0.3.14"," (",[14,38,39],{},"pip install py-spy","); ",[14,42,43],{},"0.3.x"," is required for reliable ",[14,46,47],{},"--pid"," attach, ",[14,50,51],{},"dump",", and ",[14,54,55],{},"--native",".",[31,58,59],{},"A running CPython process you can identify by PID.",[31,61,62,63,66,67,70,71,56],{},"On Linux: ",[14,64,65],{},"CAP_SYS_PTRACE",", root, or ",[14,68,69],{},"kernel.yama.ptrace_scope=0"," to attach to a process you do not own. In Docker, run the container with ",[14,72,73],{},"--cap-add SYS_PTRACE",[31,75,76,77,56],{},"macOS requires running py-spy with ",[14,78,79],{},"sudo",[23,81,83],{"id":82},"solution","Solution",[10,85,86],{},"First locate the worker's PID — pick the actual interpreter, not a shell or supervisor parent:",[88,89,94],"pre",{"className":90,"code":91,"language":92,"meta":93,"style":93},"language-bash shiki shiki-themes github-light github-dark","pgrep -f 'gunicorn.*worker' || ps -eo pid,cmd | grep '[p]ython'\n","bash","",[14,95,96],{"__ignoreMap":93},[97,98,101,105,109,113,117,120,123,126,129,132],"span",{"class":99,"line":100},"line",1,[97,102,104],{"class":103},"sScJk","pgrep",[97,106,108],{"class":107},"sj4cs"," -f",[97,110,112],{"class":111},"sZZnC"," 'gunicorn.*worker'",[97,114,116],{"class":115},"szBVR"," ||",[97,118,119],{"class":103}," ps",[97,121,122],{"class":107}," -eo",[97,124,125],{"class":111}," pid,cmd",[97,127,128],{"class":115}," |",[97,130,131],{"class":103}," grep",[97,133,134],{"class":111}," '[p]ython'\n",[10,136,137,140],{},[14,138,139],{},"py-spy top"," gives a live, auto-refreshing view of the hottest functions in that process:",[88,142,144],{"className":90,"code":143,"language":92,"meta":93,"style":93},"# Live top-style view; %Own is self time, %Total includes subcalls.\npy-spy top --pid 48291\n",[14,145,146,152],{"__ignoreMap":93},[97,147,148],{"class":99,"line":100},[97,149,151],{"class":150},"sJ8bj","# Live top-style view; %Own is self time, %Total includes subcalls.\n",[97,153,155,157,160,163],{"class":99,"line":154},2,[97,156,16],{"class":103},[97,158,159],{"class":111}," top",[97,161,162],{"class":107}," --pid",[97,164,165],{"class":107}," 48291\n",[10,167,168,171],{},[14,169,170],{},"py-spy record"," samples for a window and writes an interactive flame graph SVG:",[88,173,175],{"className":90,"code":174,"language":92,"meta":93,"style":93},"# Sample for 30s and write a flame graph. --native unwinds C\u002FRust frames\n# (e.g. numpy internals) so they are not collapsed into one built-in row.\npy-spy record --pid 48291 --duration 30 --native --output flame.svg\n",[14,176,177,182,187],{"__ignoreMap":93},[97,178,179],{"class":99,"line":100},[97,180,181],{"class":150},"# Sample for 30s and write a flame graph. --native unwinds C\u002FRust frames\n",[97,183,184],{"class":99,"line":154},[97,185,186],{"class":150},"# (e.g. numpy internals) so they are not collapsed into one built-in row.\n",[97,188,190,192,195,197,200,203,206,209,212],{"class":99,"line":189},3,[97,191,16],{"class":103},[97,193,194],{"class":111}," record",[97,196,162],{"class":107},[97,198,199],{"class":107}," 48291",[97,201,202],{"class":107}," --duration",[97,204,205],{"class":107}," 30",[97,207,208],{"class":107}," --native",[97,210,211],{"class":107}," --output",[97,213,214],{"class":111}," flame.svg\n",[10,216,217,220],{},[14,218,219],{},"py-spy dump"," prints a single snapshot of every thread's current stack — the fastest answer to \"what is this hung process stuck on\":",[88,222,224],{"className":90,"code":223,"language":92,"meta":93,"style":93},"# One-shot stack dump of all threads; no sampling window needed.\npy-spy dump --pid 48291\n",[14,225,226,231],{"__ignoreMap":93},[97,227,228],{"class":99,"line":100},[97,229,230],{"class":150},"# One-shot stack dump of all threads; no sampling window needed.\n",[97,232,233,235,238,240],{"class":99,"line":154},[97,234,16],{"class":103},[97,236,237],{"class":111}," dump",[97,239,162],{"class":107},[97,241,165],{"class":107},[10,243,244,245,247],{},"If attach is refused on Linux, the cause is almost always ",[14,246,20],{},":",[88,249,251],{"className":90,"code":250,"language":92,"meta":93,"style":93},"# Option A (per session, needs root): allow any process to attach.\nsudo sysctl -w kernel.yama.ptrace_scope=0\n# Option B (no sysctl change): just run py-spy elevated.\nsudo py-spy dump --pid 48291\n",[14,252,253,258,274,279],{"__ignoreMap":93},[97,254,255],{"class":99,"line":100},[97,256,257],{"class":150},"# Option A (per session, needs root): allow any process to attach.\n",[97,259,260,262,265,268,271],{"class":99,"line":154},[97,261,79],{"class":103},[97,263,264],{"class":111}," sysctl",[97,266,267],{"class":107}," -w",[97,269,270],{"class":111}," kernel.yama.ptrace_scope=",[97,272,273],{"class":107},"0\n",[97,275,276],{"class":99,"line":189},[97,277,278],{"class":150},"# Option B (no sysctl change): just run py-spy elevated.\n",[97,280,282,284,287,289,291],{"class":99,"line":281},4,[97,283,79],{"class":103},[97,285,286],{"class":111}," py-spy",[97,288,237],{"class":111},[97,290,162],{"class":107},[97,292,165],{"class":107},[23,294,296],{"id":295},"why-this-works","Why this works",[10,298,299,300,303,304,307,308,311,312,314,315,317],{},"py-spy is written in Rust and reads the target process's memory through ",[14,301,302],{},"process_vm_readv"," (Linux), ",[14,305,306],{},"vm_read"," (macOS), or ",[14,309,310],{},"ReadProcessMemory"," (Windows), reconstructing Python frame objects without pausing the interpreter for more than a few microseconds per sample. Because it never injects code or imports a module into the target, it cannot corrupt application state and adds negligible overhead, which is what makes it safe to point at a live production worker. The ",[14,313,20],{}," setting exists precisely because reading another process's memory is a privileged operation; relaxing it or granting ",[14,316,65],{}," is what authorizes the attach.",[23,319,321],{"id":320},"edge-cases-and-failure-modes","Edge cases and failure modes",[28,323,324,335,345,357,381],{},[31,325,326,330,331,334],{},[327,328,329],"strong",{},"Wrong PID under a process manager."," Gunicorn, uWSGI, and Celery fork worker processes; profiling the master shows almost no work. Attach to a worker PID, or use ",[14,332,333],{},"py-spy dump --pid \u003Cmaster>"," once to discover children.",[31,336,337,340,341,344],{},[327,338,339],{},"Container PID namespaces."," A PID inside a container differs from the host PID. Either run py-spy inside the container (with ",[14,342,343],{},"SYS_PTRACE"," added) or translate the PID before attaching from the host.",[31,346,347,350,351,353,354,356],{},[327,348,349],{},"C extensions hide time."," Without ",[14,352,55],{},", time inside numpy, pandas, or a compiled dependency collapses into an opaque built-in frame. Add ",[14,355,55],{},", which needs debug symbols to be most useful.",[31,358,359,362,363,366,367,370,371,374,375,380],{},[327,360,361],{},"Idle \u002F I\u002FO-bound threads."," A worker blocked on a socket shows ",[14,364,365],{},"wait","-style frames. Use ",[14,368,369],{},"--idle"," to include idle threads in ",[14,372,373],{},"record",", and remember py-spy measures wall-clock stacks, not CPU — cross-check with ",[376,377,379],"a",{"href":378},"\u002Fsystematic-debugging-performance-profiling\u002Fcpu-profiling-with-cprofile-and-py-spy\u002Finterpreting-cprofile-cumulative-vs-total-time\u002F","cProfile and pstats"," if you need exact CPU attribution.",[31,382,383,386,387,390],{},[327,384,385],{},"Static or stripped Python builds."," py-spy may fail to locate the interpreter struct (",[14,388,389],{},"Failed to find python interpreter","). Match the py-spy version to the CPython version and avoid heavily stripped builds.",[23,392,394],{"id":393},"frequently-asked-questions","Frequently Asked Questions",[10,396,397,400],{},[327,398,399],{},"Do I need to restart my process to profile it with py-spy?","\nNo. py-spy attaches to an already-running interpreter by PID and reads its stacks from outside the process, so there is no restart and no code change. This is why it is safe for production workers.",[10,402,403,406,407,409,410,412,413,415,416,419,420,423,424,426],{},[327,404,405],{},"Why does py-spy fail with Operation not permitted on Linux?","\nLinux ",[14,408,20],{}," restricts which processes may attach to another. Run py-spy with ",[14,411,79],{},", grant the binary ",[14,414,65],{},", or set ",[14,417,418],{},"kernel.yama.ptrace_scope"," to ",[14,421,422],{},"0",". In a container, add the ",[14,425,343],{}," capability.",[10,428,429,432,434],{},[327,430,431],{},"What does the --native flag do in py-spy?",[14,433,55],{}," unwinds native C and Rust stack frames alongside Python frames, so time spent inside C extensions like numpy or a compiled library shows up instead of appearing as an opaque built-in call.",[10,436,437,438],{},"← Back to ",[376,439,441],{"href":440},"\u002Fsystematic-debugging-performance-profiling\u002Fcpu-profiling-with-cprofile-and-py-spy\u002F","CPU Profiling with cProfile and py-spy",[443,444,445],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":93,"searchDepth":154,"depth":154,"links":447},[448,449,450,451,452],{"id":25,"depth":154,"text":26},{"id":82,"depth":154,"text":83},{"id":295,"depth":154,"text":296},{"id":320,"depth":154,"text":321},{"id":393,"depth":154,"text":394},"Attach py-spy to a live Python process by PID: py-spy top for a live view, record for a flame graph SVG, dump for stacks, plus --native and ptrace_scope fixes.","md",{"slug":456,"type":457,"breadcrumb":458,"datePublished":459,"dateModified":459,"faq":460,"howto":467},"profiling-a-running-process-with-py-spy","long_tail","py-spy on a Live Process","2026-06-18",[461,463,465],{"q":399,"a":462},"No. py-spy attaches to an already-running interpreter by PID and reads its stacks from outside the process, so there is no restart and no code change. This is why it is safe for production workers.",{"q":405,"a":464},"Linux ptrace_scope restricts which processes may attach to another. Run py-spy with sudo, grant the binary CAP_SYS_PTRACE, or set kernel.yama.ptrace_scope to 0. In a container, add the SYS_PTRACE capability.",{"q":431,"a":466},"--native unwinds native C and Rust stack frames alongside Python frames, so time spent inside C extensions like numpy or a compiled library shows up instead of appearing as an opaque built-in call.",{"name":468,"description":469,"steps":470},"How to profile a running Python process with py-spy","Find the PID, then attach py-spy top, record, or dump to inspect a live process without restarting it.",[471,474,477,480,483],{"name":472,"text":473},"Find the process PID","Identify the target interpreter's PID with ps or pgrep, choosing the worker process rather than a supervisor parent.",{"name":475,"text":476},"Watch live with py-spy top","Run py-spy top --pid PID for a continuously updating view of the hottest Python functions in the live process.",{"name":478,"text":479},"Record a flame graph","Run py-spy record --pid PID --output flame.svg for a sampling window, optionally adding --duration and --native.",{"name":481,"text":482},"Dump current stacks","Run py-spy dump --pid PID to print a one-shot snapshot of every thread's stack, ideal for a hung process.",{"name":484,"text":485},"Resolve permissions if attach fails","If attach is denied, run with sudo, grant CAP_SYS_PTRACE, or relax kernel.yama.ptrace_scope.",true,"\u002Fsystematic-debugging-performance-profiling\u002Fcpu-profiling-with-cprofile-and-py-spy\u002Fprofiling-a-running-process-with-py-spy",{"title":5,"description":453},"systematic-debugging-performance-profiling\u002Fcpu-profiling-with-cprofile-and-py-spy\u002Fprofiling-a-running-process-with-py-spy\u002Findex","_cXSgb0-J98t1I1DccoULmVQmyyjHBASbdEBv6FEFXk",1781793487407]