Python

Distributed Tracing in Python with OpenTelemetry

Learn how distributed tracing with OpenTelemetry helps pinpoint performance bottlenecks in Python applications. Covers setup, manual instrumentation, common mistakes, and practical tips for using traces to optimize latency and fix slow services.

August 2026 8 min read 14 views 0 hearts

Why Distributed Tracing Matters for Python Performance

When your Python application becomes more than just a few scripts running on a laptop, you quickly realize that simple timing with time.time() doesn't tell you the whole story. Your code might be handling hundreds of requests per second across multiple services - a web server here, a background worker there, a database call somewhere else. If something slows down, where do you even start looking?

This is where distributed tracing comes in. Think of it as giving every operation in your system a unique ID that follows it through every service, every function call, every database query. When done right, you can pinpoint exactly where time is being spent across your entire application stack.

The Basics: What Distributed Tracing Actually Does

Distributed tracing creates what's called a "trace" - the complete journey of a single request through your system. Each trace is made up of "spans," which represent individual operations. For example, if your Python web app receives a request that calls a database and then sends a message to a queue, that's one trace with three spans.

Here's what a span typically contains: - A start timestamp and duration - The operation name (like "query user database" or "render template") - Any tags or metadata you want to attach (user IDs, error messages, etc.) - A trace ID linking it to the full journey

Setting Up Distributed Tracing in Python

Let's talk about OpenTelemetry, because that's the standard that most Python teams actually use now. It's not the only option, but it's the one that works across different services and languages without tying you to a specific vendor.

First, install the basics:

pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-flask

For a typical Flask application, you'd set it up like this:

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from flask import Flask

# Set up the tracer provider
provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter())
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)

# Create your Flask app and instrument it
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)

Now every request to your Flask app automatically creates spans. The real power comes when you instrument downstream calls - your database queries, HTTP requests to other services, and even background tasks.

Understanding the Output: What You Actually Learn

Once you have tracing running, you'll start seeing things you never noticed before. At PythonSkillset, I've seen teams discover that what they thought was a slow database query was actually the application spending 80% of its time in serialization code they'd written years ago and completely forgotten about.

The key metrics to look for in your traces: - Service latency breakdown - Which services in your chain are the slowest? - Error rates per operation - Are certain endpoints failing consistently? - Dependency bottlenecks - Is one external API slowing down everything? - Temporal patterns - Does performance degrade at certain times of day?

Manual Instrumentation for Custom Code

Auto-instrumentation is great for the basics, but the real insights come when you add your own spans. Maybe you have a complex data processing function that runs in the background. Here's how to trace it:

from opentelemetry import trace
import time

tracer = trace.get_tracer(__name__)

def process_customer_data(customer_id):
    with tracer.start_as_current_span("process_all_data") as parent_span:
        parent_span.set_attribute("customer.id", customer_id)

        with tracer.start_as_current_span("validate_data") as validation_span:
            # Data validation logic here
            time.sleep(0.5)

        with tracer.start_as_current_span("transform_data") as transform_span:
            # Data transformation logic here
            time.sleep(1.2)

        with tracer.start_as_current_span("save_to_database") as save_span:
            # Database save logic here
            time.sleep(0.3)
            save_span.set_attribute("rows.affected", 42)

This gives you, right in your tracing dashboard, exact durations for each sub-operation, with the customer ID as context. When your boss asks why customer processing is slow, you don't guess - you show them.

What NOT to Do with Distributed Tracing

I've seen teams make some expensive mistakes with tracing. Here are the ones that hurt most:

Tracing everything with no filtering - If you trace every single operation including debug-level spans, you'll overwhelm your storage. Start with critical paths only. You can always add more later.

Treating traces as logs - Don't store error messages, full request bodies, or anything PII-sensitive in span attributes. That's what logging is for. Traces should be high-level enough to point you to the problem, not contain all the details.

Forgetting about sample rates - In production, you probably don't need to trace every single request. Most tracing systems default to a sensible sample rate (like 1%), but check yours. Tracing 100% of requests at high volume is expensive.

Ignoring the cost of instrumentation - Each span creation and export has overhead. In high-throughput systems, this can become measurable. Profile your tracing overhead before rolling it out to production.

Making Distributed Tracing Actually Useful

The biggest mistake people make is setting up tracing and then never looking at the data. Here's what makes tracing valuable at PythonSkillset:

Start with a specific performance problem - Don't instrument "everything just in case." Pick one endpoint or background task that's been slow, instrument that path thoroughly, and fix it. Then move to the next.

Connect traces to your incident response - When something breaks, the trace should be the first place you look. Make sure your alerting systems include trace IDs in notifications.

Use traces to validate performance changes - Before you refactor a slow function, capture its traces. After your changes, compare the spans. This is the only way to know if your optimization actually worked.

Let traces guide your architecture - If you consistently see one service slowing down because of chatty communication with another, maybe it's time to consider merging them or adding caching.

The Bottom Line

Distributed tracing transforms how you understand performance from "I think this part is slow" to "here's the exact 47 milliseconds wasted in serialization." It takes some setup time, but for any Python system that spans more than one process or service, it's not optional - it's how you keep your application running fast without guessing.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.