Every profiling story so far has the same precondition: you must be able to reproduce the problem, locally, under instrumentation. That precondition fails exactly when it matters — the pathological input only occurs in production, the slowdown only appears at 400 requests per second, the leak only reproduces after six hours.
PEP 799 removes the precondition. Python 3.15 ships Tachyon, a statistical sampling profiler in the standard library that can attach to an already-running process with no code change, no restart, and — the PEP’s claim — “zero measurable overhead on the target process.”
python -m profiling.sampling run script.py
python -m profiling.sampling attach 12345
python -m profiling.sampling dump 12345
python -m profiling.sampling replay recording.bin
What it does
It samples the target’s stacks from outside the process, reading memory rather than instrumenting the interpreter. Default rate 1 kHz, configurable up to 1 MHz. The PEP describes it as “the fastest sampling profiler available for Python (at the time of its contribution)”.
The modes are where the operational value is:
-
wall(default) — where wall-clock time goes, including blocking I/O. What you want for “why is this endpoint slow”. -
cpu— only samples where the thread is on-CPU. Separates compute from waiting. -
gil— samples which thread holds the GIL. This is the first stdlib answer to “which of my threads is serialising everything”, a question that previously required guessing. -
exception— samples where exceptions are being raised, which finds the hot try/except nobody knew about.
Outputs: pstats (so existing tooling works), collapsed stacks, flamegraph, diff-flamegraph (before/after in one image), gecko, heatmap, a --live terminal UI, and a binary record-and-replay format so you can capture during the incident and analyse afterwards. --async-aware reconstructs asyncio task stacks rather than showing you the event loop.
It complements PEP 831, which makes 3.15 builds use frame pointers by default — that is what lets native profilers unwind through the interpreter.
The constraints to plan for before the incident
This is the part to read twice, because every one of them is discovered at the worst possible moment:
The profiler and the target must run the same Python minor version. For pre-releases, the same exact version. The profiler reads the target’s internal structures, and those change between versions. If your production image is 3.15.2 and your jump box has 3.15.0, you cannot attach.
You cannot mix free-threaded and standard builds. A python3.15t target needs a python3.15t profiler.
Permissions. Linux needs root, CAP_SYS_PTRACE, or a relaxed /proc/sys/kernel/yama/ptrace_scope. macOS needs root or the debugger entitlement. Windows needs SeDebugPrivilege. Container runtimes drop CAP_SYS_PTRACE by default.
Put together, those three mean the incident-time question is not “shall we profile?” but “did somebody, months ago, ensure that a matching interpreter exists somewhere that can see this process and has ptrace?” If the answer is no, the answer at 3 a.m. is also no.
The concrete preparation: ship a debug sidecar or a jump path with the exact same interpreter build as production, and decide deliberately whether CAP_SYS_PTRACE is acceptable in your threat model. It is a real trade — the capability that lets you read a process’s memory to profile it is the capability that lets you read its secrets.
💡Your service is on Python 3.13 and will be for at least a year. What is the equivalent capability today, and what do you give up? click to reveal
py-spy. It is a third-party sampling profiler that attaches to a running process by PID without instrumentation, works on 3.12–3.14, and has been the standard answer for this since long before PEP 799. It gives you py-spy dump for an instant stack snapshot, py-spy top for a live view, and py-spy record for a flamegraph.
What you give up relative to Tachyon: it is a dependency to vet and install rather than a stdlib module; it has no gil mode, so “which thread is holding everything up” is still a guess; and its asyncio support is weaker.
What you keep: the same operational constraints, essentially unchanged. py-spy also needs ptrace permissions on Linux and the debugger entitlement or root on macOS, and it also needs to understand the target’s interpreter version. So the preparation work — a way to reach the process, with permission, from a compatible environment — transfers directly. Doing that work now on 3.13 is not throwaway; it is the same work you would do for 3.15.
What it is not for
The documentation is explicit on both ends of the range. Not for sub-second scripts — a statistical profiler needs samples, and at 1 kHz a 200 ms script gives you 200 of them, which is not a distribution. And not for resolving a 1-2% microbenchmark difference; the docs redirect you to timeit, for the reasons in article 11.18.
Sampling profilers answer “where is this long-running process spending its time, right now, in production”. That is a question nothing else in the stdlib could answer, and it is the question you have during an incident.