Skip to content
← Back to explainers

Python explainer

Sample a Running Program with Tachyon in Python 3.15

About 4 min to read. Allow extra time to work through examples.

Attach Python 3.15's built-in sampling profiler to a running process, pick what to sample, and learn what it needs to attach.

This article uses Python. You can make it your practice mode to browse related content across the site.

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

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.

step 36 of 38

Memory, Copies and Slow Code

Up next Check Whether Your Python Has a JIT, Then Measure