A worker process has run for six hours, and now it uses a full CPU core
and makes no progress. Restarting it under cProfile would throw away the
state that causes the problem. Python 3.15 ships a profiler, called
Tachyon, that attaches to the process while it runs:
python3.15 -m profiling.sampling attach -d 10 12345 # sample process 12345 for 10 seconds
python3.15 -m profiling.sampling dump 12345 # print its stack once
python3.15 -m profiling.sampling run script.py # start a script under the profiler
Python 3.15.0 came out on 9 October 2026, as its release page shows. These commands do not exist in Python 3.14 or earlier.
How a sampling profiler works
cProfile hooks every function call, which makes the program slower.
Tachyon is a sampling profiler. It reads the target's call stack from
outside, 1,000 times a second by default, and counts the functions it
sees. A function that shows up in 30% of the samples was running about 30%
of the time. Your program runs at close to its normal speed, and you can
attach without a restart.
Sampling has two limits. It gives shares of time, not exact call counts. And a short run gives few samples, so let it run long enough to repeat the slow work many times.
Choose what to sample
By default Tachyon samples only the main thread. Add -a to sample every
thread, as in attach -a -d 10 12345. The --mode option picks which
samples count:
| Mode | Counts a sample when the thread |
|---|---|
wall |
is doing anything, including waiting (the default) |
cpu |
is running on a CPU |
gil |
holds the GIL |
exception |
has an active exception |
Use wall to see where a slow request spends its time, and cpu to see
what keeps a core busy. Add --flamegraph to save an HTML flame graph, or
--live for a view that updates like top. The
sampling profiler documentation
lists every option.
Make sure you can attach
Reading another process's memory needs permission from the operating
system. On macOS, even run stops with a permission error unless you use
sudo. On Linux, attaching needs ptrace permission, which many containers
block.
The profiler and the target must also be the same Python minor version, such as 3.15. A pre-release must match exactly, and a standard build cannot profile a free-threaded one, or the other way round. Rehearse in your real production setup, with the real interpreter and container, before an incident.
Check your understanding
You run dump on the stuck worker, and its stack shows json.loads. Is JSON parsing the problem?
Show answer
Hide answer
Not yet. A dump shows one moment, and the worker could be passing through
json.loads by chance. Sample for 10 seconds or more and see what share of
the samples land there. If most of them do, look at what the worker is
parsing, such as a payload that grew far larger than usual.
On Python 3.14 and earlier
Use py-spy, from the previous article.
It also samples a running process from outside, with dump, top and
record commands. It has its own rules about which Python versions it
supports, and it needs the same permission to read another process.
Check the fix without the profiler
Use the samples to find the slow place. Then make one change and time it with the profiler off, on the same workload. Keep the sampling settings with each profile, so you can compare a later profile with it.
Going further
PEP 799 adds the profiling package
that holds Tachyon, as profiling.sampling, and cProfile, as
profiling.tracing.