Python 3.15 Tachyon Profiler: A Hands-On Guide

Python 3.15 Tachyon Profiler: A Hands-On Guide Python

Python 3.15 adds Tachyon, a statistical sampling profiler available as profiling.sampling. It can launch a script or attach to an already-running Python process, then estimate where time is spent by reading stack samples instead of instrumenting every function call.

The practical payoff is straightforward: use Tachyon when you need to find broad CPU or latency hotspots with minimal disturbance to the target process. Use profiling.tracing when you need exact call counts, and use timeit for small benchmark comparisons. As of August 2026, Python 3.15.0rc1 is a preview release, so test the workflow now but wait for the final 3.15 release before adopting it in production.

What Tachyon Changes in Python Profiling

Traditional deterministic profilers record every function call and return. That produces exact call counts, but the instrumentation changes how the application runs and can add meaningful overhead.

Tachyon takes periodic snapshots of the target's call stack from outside the process. Functions that appear in more samples are likely consuming more elapsed or CPU time. The result is statistical rather than exact, but it is usually the better tool for answering: “Where is this long-running process spending its time?”

Python 3.15 also groups the standard profiling tools under one namespace:

  • profiling.sampling: Tachyon's statistical sampler.
  • profiling.tracing: deterministic tracing, relocated from cProfile.
  • cProfile: retained as a compatibility alias.
  • profile: deprecated in 3.15 and scheduled for removal in 3.17.

This article focuses on the sampler. The broader Python 3.15 migration changes are covered separately in the Python 3.15 upgrade guide.

Start with a Workload You Can Interpret

The following script deliberately mixes Python computation, native hashing, and a short wait. That makes the difference between wall-clock and CPU sampling visible.

import hashlib
import time


def cpu_hotspot(rounds: int) -> int:
    total = 0
    for value in range(rounds):
        total += (value * value) % 97
    return total


def native_hashing(rounds: int) -> bytes:
    digest = b"tachyon"
    for _ in range(rounds):
        digest = hashlib.sha256(digest).digest()
    return digest


def simulated_io(delay: float) -> None:
    time.sleep(delay)


def main() -> None:
    cpu_hotspot(12_000_000)
    native_hashing(700_000)
    simulated_io(0.7)
    print("workload complete")


if __name__ == "__main__":
    main()

Save it as workload.py, then run the default profile:

python -m profiling.sampling run --limit=8 workload.py

The default output is a table similar to pstats. Important columns include:

  • nsamples: direct samples and cumulative samples.
  • sample%: the share of samples where the function was executing directly.
  • tottime: estimated direct time derived from sample count.
  • cumtime: estimated time including called functions.

Do not interpret these estimates as stopwatch measurements. Sampling results vary slightly between runs, and short functions can complete between samples.

Measured Results on Python 3.15.0rc1

I ran the example on Windows 11 x64 with the official CPython 3.15.0rc1 embeddable runtime. The profiler used its default 1 kHz rate. One wall-clock run captured 2,125 samples over 2.13 seconds at 999.78 samples per second.

| Function | Direct samples | Share | Estimated direct time | |—|—:|—:|—:| | simulated_io | 700 | 33.2% | 700 ms | | cpu_hotspot | 684 | 32.4% | 684 ms | | native_hashing | 594 | 28.1% | 594 ms |

The wait appears prominently because the default is wall-clock mode. The stack still points to simulated_io while the process sleeps, which is useful when investigating end-to-end latency.

Next, I ran the same script in CPU mode:

python -m profiling.sampling run --mode=cpu --limit=8 workload.py

That run captured 568 CPU samples. simulated_io disappeared, while cpu_hotspot accounted for 52.4% of direct samples and native_hashing for 40.5%. The exact percentages will vary, but the interpretation is stable: CPU mode filters out time when the thread is not executing on a CPU.

What the Comparison Tells You

Run both modes when a slow request could be caused by either computation or waiting:

  • High in wall mode and CPU mode: investigate algorithms or computational work.
  • High only in wall mode: investigate network, disk, locks, or other waits.
  • High cumulative time but low direct time: inspect the callees beneath that function.

The CPU-mode test also reported missed samples on this short Windows workload. That is a useful warning against treating a two-second profile as a precise benchmark. For production analysis, collect a longer window under representative load and focus on large patterns rather than small percentage differences.

Generate a Flame Graph

Tachyon can emit a self-contained interactive flame graph:

python -m profiling.sampling run --flamegraph -o profile.html workload.py

The command was executed against the sample above and produced a 636,347-byte HTML report containing 2,087 usable samples. Open profile.html in a browser and look for the widest frames; width represents the estimated share of sampled time.

Other useful output formats include:

python -m profiling.sampling run --heatmap workload.py

python -m profiling.sampling run --collapsed workload.py

python -m profiling.sampling run --binary -o profile.bin workload.py
python -m profiling.sampling replay --flamegraph -o replay.html profile.bin

Binary capture is useful when you want to retain one sampling session and replay it into several views without profiling the application again.

Attach to a Running Python Process

For a server or worker that is already running, pass its process ID to attach:

python -m profiling.sampling attach -d 20 --limit=30 12345

For a one-time stack snapshot, use dump:

python -m profiling.sampling dump -a 12345

dump is especially useful when a process looks hung and you want to see what its threads are doing without collecting a full profile.

Attaching has important operating-system requirements:

  • Linux commonly requires root, CAP_SYS_PTRACE, or an appropriate Yama ptrace policy.
  • macOS commonly requires root or a debugger entitlement.
  • Windows requires administrator rights or SeDebugPrivilege to read another process.

The profiler and target must use the same Python minor version. When either side is a prerelease build, both must use the exact same prerelease. Standard and free-threaded builds also cannot be mixed for attachment.

Choose the Right Sampling Mode

Tachyon supports four modes through --mode:

| Mode | Best question | |—|—| | wall | Where does elapsed time pass, including waits? | | cpu | Where is CPU execution happening? | | gil | Where does a thread hold the GIL? | | exception | Where is time spent while exceptions are active? |

Wall mode is the safest starting point for request latency. CPU mode is better when you already know the process is compute-bound. GIL mode can help distinguish Python bytecode work from native code or waits, while exception mode is useful for codebases that may be using exceptions as control flow.

For multithreaded programs, add --all-threads:

python -m profiling.sampling run --all-threads app.py

For asyncio applications, use task-aware stack reconstruction:

python -m profiling.sampling run --async-aware --flamegraph -o async-profile.html app.py

Async-aware mode requires asyncio to be loaded in the target. It is not compatible with every other option, including --all-threads, --native, and CPU or GIL modes, so keep the first async profile simple.

A Safe Production Profiling Workflow

Tachyon avoids instrumenting the target, but the profiler still consumes resources and attachment requires sensitive process-memory permissions. Treat production profiling as an operational change.

  1. Confirm that the profiler and target versions match.
  2. Start with a 10–30 second duration under representative, non-peak load.
  3. Use the default 1 kHz rate before increasing it.
  4. Capture wall mode first, then CPU mode if the result suggests computation.
  5. Save a flame graph or binary profile outside the public web root.
  6. Restrict profile artifacts because function names, paths, and stack data may expose implementation details.
  7. Repeat the capture before and after an optimization; do not compare tiny differences from single runs.

Avoid --blocking unless non-blocking sampling produces inconsistent stacks. Blocking mode suspends the target for each sample and can create noticeable slowdown, especially at high sample rates.

Common Failure Modes

Permission Denied When Attaching

The profiler reads another process's memory. Run it with the minimum required debugger capability and follow your operating system's process-inspection policy. Do not disable platform security controls globally just to make a one-off profile easier.

Version Mismatch

An attachment from Python 3.14 to 3.15 is unsupported. During the release-candidate phase, even two different 3.15 prereleases may be incompatible. Use the exact same interpreter build for the profiler and target.

Too Few Samples

Very short scripts may finish before the profiler collects enough data. Run a representative workload for longer, loop the operation, or use profiling.tracing for exact function-call analysis. Use timeit when comparing micro-optimizations.

Misreading Wall Time as CPU Time

A sleeping or blocked function can dominate a wall profile. That does not mean its Python code is computationally expensive. Compare with --mode=cpu before changing the algorithm.

When to Use Tachyon Instead of Other Tools

Use Tachyon for long-running services, workers, data pipelines, concurrent applications, and production-like workloads where the main goal is hotspot discovery.

Use profiling.tracing when you need exact call counts or a complete call graph. Use timeit or a dedicated benchmark harness when differences are only a few percent. Statistical profiling is designed to guide investigation, not to replace controlled performance measurement.

Python 3.15's new profiler closes an important gap in the standard library: it gives developers a practical, low-disturbance way to inspect running applications without adding profiling hooks to application code. Start with wall mode, compare CPU mode, and move to flame graphs once the basic hotspot pattern is clear.

Official References

Comment

Copied title and URL