Finding a performance problem in a production Python application is often harder than fixing it.

A request may become slow only under real traffic. CPU usage can increase without an obvious application error. A background worker may suddenly consume more CPU. An API can spend most of its time inside a native extension instead of Python code.

Profiling helps answer these questions, but production profiling has an important challenge: the profiler needs to reconstruct the call stack accurately.

Python 3.15 changes the situation by enabling frame pointers by default in CPython builds on platforms that support them. This makes native stack unwinding more reliable for profilers, debuggers, crash analysis tools, and system-level observability tools such as perf and eBPF.

The change does not add a new Python API that application developers have to learn. Instead, it improves the runtime's observability.

That makes Python 3.15 particularly interesting for teams troubleshooting performance problems in production.

What Is a Frame Pointer?

A frame pointer is a register used to help describe the current function's stack frame.

When a native program executes a sequence such as:

API request
    ↓
Python function
    ↓
Python function
    ↓
C extension
    ↓
Native function

a profiler needs to reconstruct that call chain.

Without reliable stack information, the profiler may have difficulty showing the complete path.

With frame pointers, a profiler can follow the frame-pointer chain through native stack frames.

Python 3.15 enables frame pointers by default where the compiler and platform support them. The default build uses compiler options such as:

-fno-omit-frame-pointer
-mno-omit-leaf-frame-pointer

The exact flags depend on the platform and compiler.

Why Does This Matter for Python?

Python applications rarely consist only of Python code.

A typical production service may use:

Python application
       ↓
CPython runtime
       ↓
C extensions
       ↓
OpenSSL
       ↓
libc
       ↓
Operating system

Database drivers, compression libraries, cryptographic libraries, numerical packages, and other extensions can execute significant amounts of native code.

A Python-level profiler may tell you:

process_request()

is consuming time.

A system profiler can potentially provide a much deeper picture:

process_request()
    ↓
parse_payload()
    ↓
json processing
    ↓
native implementation
    ↓
system call

That additional context can make the difference between knowing which Python function is slow and understanding why it is slow.

What Changed in Python 3.15?

PEP 831 makes frame pointers enabled by default for CPython builds on supported platforms. The change is intended to improve system-level observability.

Python's documentation explains that frame pointers allow profilers, debuggers, and system tracing tools to walk the C call stack without depending on DWARF metadata for that purpose.

The change also propagates the relevant compiler configuration through Python's sysconfig, which is important for native extensions.

That means the goal is not just to make the CPython executable easier to profile.

The goal is to keep the native call stack usable across the application and its extensions.

Frame Pointers and Production Profiling

Production profiling is different from profiling a small local script.

A developer can often reproduce a slow operation locally:

Run application
↓
Run profiler
↓
Reproduce problem
↓
Inspect results

Production problems are different.

The issue may happen only:

  • During peak traffic

  • With a particular customer

  • After a deployment

  • With a specific workload

  • When a queue becomes large

  • When several services interact

  • Under a particular database response time

  • When garbage collection increases

Reproducing that exact environment locally may be impossible.

This is where low-overhead statistical profiling becomes valuable.

Python 3.15 also introduces a dedicated profiling package that organizes Python's profiling tools and includes statistical sampling and deterministic tracing approaches. The documentation recommends statistical sampling for most performance analysis because it has low overhead and can be used in production scenarios.

Sampling vs Deterministic Profiling

There are two important profiling approaches.

Statistical Sampling

A sampling profiler periodically looks at what the program is doing.

Conceptually:

Sample 1 → function_a()
Sample 2 → function_a()
Sample 3 → function_b()
Sample 4 → function_a()
Sample 5 → function_c()

After enough samples, the profiler can estimate where the application spends its time.

This approach generally has much less overhead than tracing every function call.

Deterministic Tracing

A tracing profiler records function calls and returns:

call function_a()
call function_b()
return function_b()
call function_c()
return function_c()
return function_a()

This provides more exact information but introduces more instrumentation overhead.

Python's new profiling documentation recommends sampling for most performance investigations and deterministic tracing when exact call counts are important.

For production systems, that distinction matters.

You usually want enough information to identify the bottleneck without changing application behavior significantly.

Using perf With Python 3.15

On Linux, perf is a useful system-level profiling tool.

A simplified profiling workflow looks like:

perf record -g -- python app.py

After the workload completes:

perf report -g

The -g option requests call-graph information.

With frame pointers available, the profiler can unwind native call stacks more efficiently.

Python's profiling documentation explains that frame pointers provide more reliable stack unwinding and are particularly useful for perf profiling.

For a production service, you would normally profile a representative process or workload rather than simply running the entire application under a profiler indefinitely.

Checking Whether Frame Pointers Are Enabled

Python provides a way to inspect its build configuration.

For example:

python -m sysconfig | grep 'no-omit-frame-pointer'

If the relevant configuration appears, the interpreter was built with the frame-pointer flags.

This is useful when diagnosing profiling differences between environments.

For example, your development environment might use:

Python 3.15
Frame pointers: enabled

while a custom production build might have been compiled differently.

The Python documentation recommends checking the build configuration when troubleshooting perf profiling behavior.

Why Custom Python Builds Matter

Many teams do not use the Python executable exactly as provided by the operating system.

They may build Python themselves for:

  • Custom compiler options

  • Container images

  • Performance tuning

  • Security requirements

  • Embedded systems

  • Specialized Linux distributions

Python 3.15 provides an explicit configuration option:

./configure --without-frame-pointers

This disables the default frame-pointer behavior.

In most cases, you should have a specific reason before disabling it.

If production observability is important, removing frame pointers can make native profiling more difficult.

Native Extensions Are Important

Consider a Python application using a database driver.

The application code might look simple:

def load_customer(customer_id):
    return database.execute(
        "SELECT * FROM customers WHERE id = %s",
        (customer_id,)
    )

The Python code itself may not be where the CPU time is spent.

Execution can move through:

load_customer()
    ↓
database driver
    ↓
C extension
    ↓
network library
    ↓
operating system

A Python-only view may not provide enough information.

Native stack visibility can help connect the Python call with the underlying native work.

This is one reason Python 3.15's frame-pointer change is relevant beyond the CPython interpreter itself.

Why Extension Build Flags Matter

Python 3.15 exposes its frame-pointer configuration through sysconfig.

The intention is that native extensions built using the Python build configuration inherit the appropriate settings. Python's documentation specifically notes that extension modules and custom native build systems should preserve the frame-pointer configuration so the unwind chain remains intact.

This matters because an application can have a perfect frame-pointer chain inside CPython but lose useful information when execution enters a native component that was compiled differently.

For example:

Python
  ↓
CPython
  ↓
Extension A
  ↓
Extension B
  ↓
Native library

If one component breaks the unwind chain, the profiler may not be able to reconstruct the complete stack.

A Practical Production Investigation

Suppose a Python API becomes slow after traffic increases.

Application metrics show:

Average response time: 900 ms
CPU usage:              85%
Error rate:              0.1%

There are no obvious exceptions.

The first mistake would be to immediately rewrite application code.

Instead, start with profiling.

Step 1: Identify the Affected Process

Find the Python process handling the workload.

For example:

ps aux | grep python

In a containerized environment, use the appropriate container or orchestration tooling to identify the process.

Step 2: Capture a Representative Profile

Use a low-overhead sampling approach appropriate for the environment.

With perf, for example:

perf record -g -p <PID>

Run the profiler for a controlled period and stop it after collecting enough samples.

Step 3: Inspect the Call Graph

perf report -g

Look for functions that consistently appear near the top of the profile.

For example:

Samples
--------------------------------
40%  process_request
25%  parse_payload
18%  database_driver
10%  compression
 7%  other

This immediately gives you a direction for further investigation.

Step 4: Follow the Hot Path

Suppose the profile shows:

process_request
    ↓
parse_payload
    ↓
compression

Do not assume compression is the root problem.

Ask why the application is compressing so much data.

The actual problem could be:

Large response
    ↓
Repeated serialization
    ↓
Compression
    ↓
High CPU

Profiling identifies the hot path.

Application analysis determines why that path exists.

Use CPU Time and Wall-Clock Time Differently

A production service can be slow without consuming much CPU.

For example:

Request
 ↓
Database call
 ↓
Wait 500 ms
 ↓
Return

The request takes 500 milliseconds, but the CPU may have done very little work during the wait.

A CPU profile may not show the database wait as the dominant cost.

That is why profiling should be combined with:

  • Request latency

  • Database metrics

  • Network metrics

  • CPU utilization

  • Memory usage

  • I/O metrics

  • Application logs

Python 3.15's profiling package supports different profiling modes, including wall-clock and CPU-oriented sampling.

The right profiling mode depends on the question you are trying to answer.

CPU Profiling Example

Imagine:

API latency: 800 ms
CPU time:    700 ms

CPU profiling is likely useful.

But if you see:

API latency: 800 ms
CPU time:    80 ms

the application is probably spending much of the request waiting on something else.

In that case, increasing CPU profiling may not explain the entire latency problem.

The distinction is simple:

CPU profile
    ↓
What is consuming processor time?

Wall-clock profile
    ↓
Where is elapsed time being spent?

Both can be useful.

Finding Python and Native Work Together

One of the strongest use cases for better native stack unwinding is mixed Python/native workloads.

Consider:

def generate_report(records):
    transformed = transform(records)
    return compress_report(transformed)

Suppose compress_report() eventually calls a native compression library.

The profiler may provide a call chain that connects:

generate_report()
    ↓
compress_report()
    ↓
Python extension
    ↓
native compression

Without reliable native stack information, the bottom part of the call chain may be incomplete.

This is especially useful when application performance depends heavily on libraries implemented in C, C++, Rust, or other native languages.

eBPF and Python

Frame pointers are also relevant to Linux eBPF-based observability.

Python's documentation specifically identifies eBPF as one of the system-level tools that benefits from frame-pointer-based stack walking.

This enables a broader observability model:

Application
     ↓
Python
     ↓
Native extension
     ↓
Kernel

Instead of looking only at Python-level metrics, system-level tooling can provide additional context about CPU and native execution.

For production engineers, this is valuable because performance problems often cross application boundaries.

Python 3.15 Does Not Automatically Make Applications Faster

This distinction is important.

Frame pointers are primarily an observability feature.

They do not automatically optimize:

for item in items:
    process(item)

They do not make database queries faster.

They do not reduce network latency.

They do not automatically improve algorithmic complexity.

The benefit is indirect:

Better observability
       ↓
Better profiling
       ↓
Better bottleneck identification
       ↓
Better optimization decisions

That is the real value.

There Is a Small Runtime Cost

Frame pointers are not completely free.

PEP 831 reports measured overhead below 2% geometric mean for typical workloads, while noting that individual workloads can behave differently.

The important point is that this is a tradeoff.

You accept some runtime cost in exchange for significantly better system-level observability.

The actual impact should always be measured for workloads where performance margins are extremely tight.

Do not assume a benchmark from another system represents your application.

Profiling in Containers

Modern Python applications are often deployed in containers.

For example:

Kubernetes
    ↓
Pod
    ↓
Container
    ↓
Python process

The profiling workflow therefore needs to account for the container environment.

Before profiling, verify:

  • The profiler can access the target process.

  • Required Linux capabilities are available.

  • The container has appropriate permissions.

  • The Python build has frame pointers enabled.

  • The host kernel supports the required profiling functionality.

  • Profiling overhead is acceptable.

Do not give every production container broad privileges simply to make profiling easier.

Instead, establish a controlled troubleshooting workflow.

Profiling a Python Service Safely

A good production profiling process looks like this:

1. Identify the performance symptom
2. Confirm the affected workload
3. Select a low-overhead profiler
4. Capture a short representative sample
5. Inspect the call graph
6. Identify the hot path
7. Correlate with application metrics
8. Make one optimization
9. Repeat the measurement
10. Compare results

This prevents profiling from becoming another source of production instability.

Common Mistakes

Profiling Only in Development

A local profile may not represent production traffic.

Production problems often depend on real data and real concurrency.

Looking Only at Python Functions

If your application relies heavily on native libraries, the Python stack alone may not tell the complete story.

Assuming High CPU Means Python Code Is the Problem

The CPU may be consumed by:

  • C extensions

  • Serialization

  • Compression

  • Cryptography

  • Database drivers

  • Native libraries

Use system-level profiling when appropriate.

Treating Profiling Data as a Benchmark

Profiling answers:

Where is time being spent?

Benchmarking answers:

How fast is this implementation under controlled conditions?

They are related but different activities.

Python's documentation explicitly distinguishes profiling from benchmarking and recommends timeit for benchmark measurements.

Running Heavy Profilers for Too Long

A profiler itself consumes resources.

Use the least intrusive method that can answer the question.

Disabling Frame Pointers Without a Reason

Python 3.15 enables them by default on supported platforms.

If you disable them in a custom build, document why and understand the impact on your observability tooling.

Best Practices

When using Python 3.15 in production:

  1. Keep frame pointers enabled unless there is a demonstrated reason not to.

  2. Verify the Python build configuration before troubleshooting profiler output.

  3. Use statistical sampling for most production performance investigations.

  4. Use deterministic tracing when exact function-call information is required.

  5. Combine CPU profiles with wall-clock and application metrics.

  6. Profile native extensions when Python code alone does not explain the workload.

  7. Preserve frame-pointer compiler settings when building native extensions.

  8. Capture short, representative production profiles.

  9. Compare profiles before and after an optimization.

  10. Avoid treating profiling results as benchmark results.

  11. Correlate profiler data with database, network, memory, and request metrics.

  12. Keep profiling permissions as narrow as practical in containerized environments.

Python 3.15 Profiling Workflow

A practical production workflow can be summarized like this:

Stage

Question

Application metrics

What is slow?

Profiling

Where is time being spent?

Call graph

Which path is hot?

Native stack

Is the work inside an extension or library?

System metrics

Is the bottleneck CPU, I/O, or waiting?

Optimization

What single change should be tested?

Re-profile

Did the change actually help?

This prevents teams from optimizing based on assumptions.

Advantages and Limitations

Advantages

  • Better native stack unwinding

  • Improved system-level profiling

  • Better visibility into C extensions and native libraries

  • More useful perf and eBPF profiling

  • No application-code changes required

  • Enabled by default on supported Python 3.15 builds

  • Better foundation for production performance investigations

Limitations

  • Frame pointers introduce some runtime and code-size overhead.

  • They do not automatically improve application performance.

  • Profiling still requires appropriate operating-system permissions.

  • Native libraries built with incompatible settings can still interrupt an unwind chain.

  • CPU profiling cannot explain every type of latency.

  • Profiling results still require engineering judgment.

Summary

Python 3.15 makes an important change that may not be visible in everyday application code: CPython enables frame pointers by default on supported platforms.

The change improves the ability of system-level tools to reconstruct native call stacks, which is particularly useful for perf, eBPF-based observability, debuggers, and applications that depend heavily on C or other native extensions.

For production teams, the biggest benefit is better visibility when a performance problem cannot be reproduced easily on a developer machine.

The practical workflow is straightforward: identify the performance symptom, capture a representative profile, inspect the Python and native call stack, correlate the result with application metrics, make a controlled change, and profile again.

Frame pointers do not make Python applications faster by themselves. They make it easier to understand where the application is spending its time, which can lead to better performance decisions.