When a Python application becomes slow, the first question is usually simple:

Which part of the code is actually using the CPU?

Finding the answer is not always simple.

A large application may have hundreds of functions, background threads, database operations, async tasks, and third-party libraries. Looking at the source code and guessing where the problem is can easily lead to optimizing the wrong thing.

Python 3.15 adds a new profiling package with a statistical sampling profiler called Tachyon. It is available through profiling.sampling and is designed to find where an application spends its time without instrumenting every function call.

That makes it particularly interesting for developers working on larger or long-running applications.

Instead of asking the application to record every function call, a sampling profiler periodically looks at what the program is doing and builds a statistical picture of where CPU time is being spent.

This article explains how Tachyon works, how to run it, how to read the results, and when statistical profiling is a better choice than traditional deterministic profiling.

What Is Tachyon?

Tachyon is Python 3.15's high-frequency statistical sampling profiler.

It is part of the new profiling package introduced through PEP 799. The package separates profiling into two main approaches:

profiling.tracing
    Deterministic profiling

profiling.sampling
    Statistical profiling

The existing cProfile functionality moves conceptually into the deterministic profiling side, while Tachyon provides the new sampling approach. cProfile remains available for compatibility.

The basic idea behind sampling is different from traditional tracing.

A tracing profiler watches function calls and returns.

A sampling profiler periodically asks:

What is the program doing right now?

If the same function appears in many samples, that is strong evidence that the function is consuming a significant amount of execution time.

How Statistical Profiling Works

Imagine an application running for ten seconds.

A sampling profiler might inspect the running process many times during those ten seconds.

A simplified example:

Sample 1 → calculate_report()
Sample 2 → calculate_report()
Sample 3 → load_data()
Sample 4 → calculate_report()
Sample 5 → calculate_report()
Sample 6 → save_report()
Sample 7 → calculate_report()
Sample 8 → load_data()

The profiler can then estimate that:

calculate_report()
    ████████████████████ 62%

load_data()
    ███████              25%

save_report()
    ███                  13%

These percentages are not exact measurements of every individual function call.

They are estimates based on the samples collected.

That is an important distinction.

Why Sampling Can Have Lower Overhead

Traditional deterministic profiling instruments function calls.

Conceptually, the profiler needs to observe events such as:

function entered
function exited
function entered
function exited

For a program that makes a very large number of function calls, this can introduce overhead.

Statistical sampling takes a different approach.

Instead of observing every call, it periodically samples the running program.

That means the profiler can collect useful CPU information without instrumenting every function call.

Python's documentation describes statistical profiling as having traditionally lower overhead because the code does not need to be instrumented.

This makes sampling particularly useful when you want to observe a realistic workload rather than significantly changing the way the workload runs.

Running Tachyon

Python 3.15 provides the profiler through:

python -m profiling.sampling

The profiler supports different operating modes, including running a program under the profiler and attaching to an already running process.

For example, a basic profiling run can be started with:

python -m profiling.sampling run app.py

For a more useful application test:

python -m profiling.sampling run -o profile.prof app.py

The exact command-line options can vary with the installed Python 3.15 release, so it is worth checking:

python -m profiling.sampling --help

before building profiling commands into scripts.

A Small Example

Consider this application:

def calculate_total(values):
    total = 0

    for value in values:
        total += value * value

    return total


def process():
    values = range(10_000_000)

    for _ in range(20):
        calculate_total(values)


if __name__ == "__main__":
    process()

Run it through the sampling profiler:

python -m profiling.sampling run app.py

Tachyon samples the application's execution while it runs.

The resulting profile can show that most of the CPU samples are associated with:

calculate_total()

That gives you a useful direction for optimization.

Without profiling, you might spend time investigating process(), file handling, or other parts of the program that are not actually responsible for most of the CPU usage.

Why Sampling Is Useful for CPU Hotspots

A profiler should help answer a practical question:

Where should I spend my optimization time?

Suppose an application contains:

API request
   ↓
Validation
   ↓
Database query
   ↓
Data transformation
   ↓
Report generation
   ↓
JSON serialization

You might assume the database query is the bottleneck.

But the profiler could show:

Database     8%
Validation   4%
Transform   61%
Report      22%
JSON         5%

Now you have evidence.

The data transformation code is the first place worth investigating.

This is one of the biggest advantages of profiling: it replaces assumptions with measurements.

Sampling Does Not Mean Every Function Is Captured

Because Tachyon samples periodically, a very short function may never appear in a sample.

For example:

def tiny_function():
    return 1

If this function executes thousands of times but each invocation is extremely short, individual calls may not be directly visible in the same way they would be in deterministic profiling.

That does not mean the profiler is broken.

It is a normal property of statistical sampling.

The profiler is answering:

Where does the application spend significant amounts of execution time?

It is not trying to record every function invocation.

Reading a Flame Graph

One of the most useful ways to visualize profiling data is a flame graph.

A simplified flame graph might look like:

main
├── process_request
│   ├── validate
│   └── generate_report
│       ├── calculate_totals
│       └── format_rows
└── cleanup

The width of a function in a flame graph represents how much sampled execution time is associated with that part of the call stack.

If you see:

generate_report
████████████████████████████

taking up most of the graph, that is a strong signal that the report-generation path deserves attention.

A wide frame does not automatically mean the function itself contains inefficient code.

The cost may come from a child function below it.

That is why you should follow the stack downward instead of immediately rewriting the top-level function.

Self Time vs Child Time

This distinction is important when reading profiling output.

Imagine:

def generate_report():
    load_data()
    calculate_totals()
    format_output()

Suppose generate_report() appears frequently in the profile.

That does not necessarily mean its own statements are expensive.

Most of the time may be spent inside:

load_data()
calculate_totals()
format_output()

A profiler helps you follow the call stack and find the deeper function responsible for the samples.

When analyzing a hotspot, ask:

  1. Is the function itself expensive?

  2. Is it calling an expensive child?

  3. Is it called too frequently?

  4. Is the workload itself too large?

These lead to very different fixes.

Profiling a Running Process

One useful feature of Tachyon is that it can attach to a running Python process.

This is valuable when the application is difficult to reproduce locally.

For example, imagine a service running with:

PID: 18452

You can use the sampling profiler's attach mode to inspect that process.

The general form is:

python -m profiling.sampling attach 18452

This approach can be useful for investigating a process that is already experiencing high CPU usage.

It is especially helpful for long-running services where reproducing the exact workload locally is difficult.

However, attaching to a production process requires appropriate operating-system permissions and should be handled carefully.

Production Profiling Requires Some Planning

Profiling production systems is different from profiling a local development script.

Before attaching to a live process, consider:

  • Process permissions

  • Security policies

  • CPU overhead

  • Duration of the profiling session

  • Sensitive information in profile output

  • Storage location of profile files

  • Whether the process can be safely inspected

Sampling is attractive because of its low-overhead design, but that does not mean you should profile production processes indefinitely.

A short profiling window during the actual problem is usually more useful than collecting huge amounts of profiling data.

Finding CPU Hotspots in Web Applications

Consider a Python web application with endpoints:

GET /users
GET /orders
GET /reports
GET /analytics

Users report that /reports is slow.

Instead of immediately rewriting the endpoint, profile a realistic request workload.

You may discover:

reports.generate
    54%

reports.transform
    21%

database client
    12%

JSON serialization
     8%

other
     5%

Now the optimization target is much clearer.

Perhaps reports.generate() repeatedly performs an expensive calculation.

The next step could be to inspect that function with a more detailed profiler or benchmark.

This is a good example of using sampling as the first step rather than treating it as the only profiling tool.

Tachyon vs cProfile

Python 3.15 gives developers more than one profiling approach.

Feature

Tachyon

cProfile / deterministic profiling

Profiling style

Statistical sampling

Deterministic

Records every function event

No

Yes

Typical overhead

Lower

Higher

Exact call counts

No

Yes

Good for CPU hotspots

Yes

Yes

Good for call-count analysis

Limited

Yes

Good for long-running workloads

Yes

Depends on workload

Good for detailed function-call analysis

Less suitable

Yes

Neither approach is universally better.

They answer slightly different questions.

Use sampling when you primarily want to understand where CPU time is being spent.

Use deterministic profiling when you need detailed function-call information.

Python's new profiling documentation explicitly separates these use cases.

When Should You Use Deterministic Profiling?

Suppose you have already found this hotspot:

calculate_totals()

Now you want to know:

How many times is this function called?

Which functions does it call?

How much cumulative time is spent underneath it?

This is where deterministic profiling becomes useful.

For example:

import profiling.tracing

profiler = profiling.tracing.Profile()

profiler.enable()

process()

profiler.disable()
profiler.dump_stats("profile.prof")

The exact API can depend on the profiling workflow you choose, but the main idea is that deterministic profiling gives you detailed function-event information.

A useful workflow is:

Sampling profiler
       ↓
Find hotspot
       ↓
Deterministic profiler
       ↓
Understand exact call behavior
       ↓
Optimize
       ↓
Benchmark again

You do not always need both, but using the right tool at the right stage can save a lot of time.

Using pstats With Profile Data

Python's pstats module can read profiling statistics generated by both deterministic and sampling profilers.

For example:

import pstats

stats = pstats.Stats("profile.prof")
stats.print_stats()

You can sort statistics by cumulative time:

import pstats

stats = pstats.Stats("profile.prof")

stats.sort_stats(
    pstats.SortKey.CUMULATIVE
).print_stats(10)

This is useful when you want a text-based view of the profiling data.

Python 3.15's pstats documentation specifically notes support for output from profiling.tracing and profiling.sampling.

Sampling Frequency Matters

A sampling profiler needs to decide how frequently to take samples.

Higher sampling frequency gives you more observations.

Lower frequency produces fewer observations.

There is a tradeoff.

For example:

Low frequency
    ↓
Fewer samples
    ↓
Less data

High frequency
    ↓
More samples
    ↓
More detail

But more samples also mean more profiling work and larger output.

For many workloads, you do not need an extremely high sampling frequency to identify a major CPU hotspot.

If one function consumes a large portion of the application's CPU time, it should show up consistently.

Short Programs Can Be Harder to Profile

Statistical profiling works best when there is enough execution time to collect meaningful samples.

Consider:

print("Hello")

The process may finish before the profiler gets enough useful observations.

Now consider a server that runs for several minutes.

There are many more opportunities to sample its execution.

This means Tachyon is particularly useful for:

  • Long-running services

  • CPU-intensive applications

  • Background workers

  • Large data-processing jobs

  • Repeated workloads

  • Applications with complex call stacks

For very short programs, benchmarking or deterministic profiling may provide more useful information.

Statistical Results Are Estimates

Suppose a profiler reports:

calculate_total    42%
load_data          35%
format_output      23%

Do not interpret that as:

calculate_total always consumes exactly 42% of CPU time.

It is an estimate based on samples.

If you run the same workload again, the exact percentages can move slightly.

For example:

Run 1:
calculate_total 42%

Run 2:
calculate_total 44%

Run 3:
calculate_total 41%

That does not necessarily indicate a performance regression.

Look for consistent patterns.

If a function repeatedly appears as the dominant hotspot, that is much more interesting than a small percentage difference between two runs.

Profiling CPU Time Is Not the Same as Finding Every Bottleneck

This is another important point.

Suppose an application spends most of its time waiting for a database:

CPU:
    15%

Waiting:
    85%

A CPU sampling profiler may correctly show that no Python function consumes a huge amount of CPU.

That does not mean the application is fast.

The bottleneck could be:

  • Database latency

  • Network latency

  • Disk I/O

  • External APIs

  • Lock contention

  • Thread scheduling

  • Resource exhaustion

Sampling is excellent for CPU hotspots, but not every performance problem is a CPU problem.

Combining Tachyon With Other Tools

A practical performance investigation can use multiple tools.

For example:

Application is slow
        ↓
Measure request duration
        ↓
Determine CPU vs I/O
        ↓
CPU is high?
        ↓
Use Tachyon
        ↓
Find hotspot
        ↓
Use deterministic profiling if needed
        ↓
Optimize
        ↓
Benchmark

For system-level investigation, Python's profiling support can also work with tools such as Linux perf and the samply profiler. Python's documentation notes that Python 3.15 adds samply support on macOS for supported configurations.

This is useful when you need to move between Python-level and native-level performance analysis.

Native Code Can Still Matter

A Python application may spend significant time inside:

  • C extensions

  • Database drivers

  • Numerical libraries

  • Compression libraries

  • Cryptographic libraries

  • Operating-system calls

A Python-level profile may show the Python function that led into that work, but understanding the native portion may require system-level profiling.

Python 3.15's profiling improvements are particularly interesting because Python functions can be represented in compatible system profilers through the interpreter's profiling support.

This gives developers a path from:

Python function
      ↓
C extension
      ↓
native code

instead of treating the Python layer as a complete performance boundary.

A Practical Profiling Workflow

Here is a workflow that works well for real projects.

Step 1: Reproduce the Problem

Do not start profiling without knowing what workload you are investigating.

For example:

python -m myapp process-large-file

Step 2: Measure the Baseline

Record:

Execution time
CPU usage
Memory usage
Request latency

depending on the application.

Step 3: Run Tachyon

Use:

python -m profiling.sampling run -o profile.prof app.py

Step 4: Inspect the Hotspots

Look for functions that repeatedly appear in a large percentage of samples.

Step 5: Verify the Hotspot

Do not immediately rewrite the function.

Check whether:

  • The function is called too often.

  • Its child functions are responsible.

  • The workload itself is too large.

  • The bottleneck is actually native code.

Step 6: Optimize One Thing

Make one meaningful change.

Step 7: Run the Same Workload Again

Compare the new result with the baseline.

This last step is critical.

A profiler tells you where to investigate. A benchmark tells you whether your change actually helped.

Common Mistakes

Optimizing the First Function You See

The widest frame is not always the actual source of the problem.

Follow the call stack.

Assuming Percentages Are Exact

Sampling produces estimates.

Look for consistent patterns instead of treating every percentage as a precise measurement.

Profiling Only Tiny Scripts

Very short programs may finish before enough samples are collected.

Use a realistic workload.

Ignoring I/O

A CPU profiler will not explain every slow database or network operation.

First determine whether CPU is actually the bottleneck.

Profiling Forever

Long profiling sessions create unnecessary data and can complicate analysis.

Start with a focused profiling window.

Changing Code Before Measuring

Without a baseline, you cannot confidently say that an optimization worked.

When Tachyon Is a Good Choice

Tachyon is particularly useful when:

  • A Python process consumes too much CPU.

  • A long-running application has an unknown CPU hotspot.

  • A workload is difficult to reproduce locally.

  • You want lower-overhead profiling.

  • You need to inspect a running process.

  • You want a quick view of where execution time is concentrated.

  • You need to profile modern Python applications without instrumenting every function call.

It is less useful when:

  • The application finishes almost immediately.

  • The problem is primarily database or network latency.

  • You need exact function-call counts.

  • You need detailed deterministic call information.

In those cases, combine it with other measurement tools.

Final Thoughts

The biggest benefit of Tachyon is not that it gives Python another profiler.

The useful part is that Python 3.15 now gives developers a built-in statistical profiling workflow designed around sampling.

That changes how you can approach performance problems.

Instead of starting with:

"I think this function is slow."

you can start with:

"Let's see where the CPU is actually spending its time."

That difference matters in large applications.

A sampling profiler can quickly narrow down a large codebase to a small number of functions worth investigating. After that, deterministic profiling, benchmarks, logs, and system-level tools can provide the additional detail needed to fix the problem.

The best performance workflow is therefore not about choosing one profiler forever. It is about using the simplest measurement tool that can answer the question you currently have.

Summary

Python 3.15 introduces Tachyon through the new profiling.sampling module. It uses statistical sampling instead of recording every function call, which makes it useful for finding CPU hotspots with relatively low profiling overhead.

It works especially well for long-running applications, background workers, data-processing jobs, and services where you need to understand where CPU time is going.

Use Tachyon to find the hot area first. Then use deterministic profiling or other tools when you need more detail. Also remember that not every performance problem is caused by CPU usage. Database calls, network delays, disk I/O, and other external factors need different kinds of investigation.

The practical approach is simple: measure first, find the hotspot, make one change, and measure again.