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:
Is the function itself expensive?
Is it calling an expensive child?
Is it called too frequently?
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 |
|
|---|---|---|
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.

Join the conversation! Your thoughts help the community grow.