DEV Community

Cover image for Distributed Tracing: Following a Request Across Microservices
Rhuturaj Takle
Rhuturaj Takle

Posted on

Distributed Tracing: Following a Request Across Microservices

Distributed Tracing: Following a Request Across Microservices

A practical guide to distributed tracing as an architectural discipline — why single-service logging and metrics stop being sufficient once a request crosses many services, how a trace actually reconstructs a request's journey, trace analysis techniques for diagnosing latency and failures, and the specific propagation challenges microservice systems built from this series' REST, gRPC, and messaging guides need to solve.


Table of Contents

  1. Introduction
  2. The Problem Distributed Tracing Solves
  3. Anatomy of a Distributed Trace
  4. Propagation Across Every Boundary a Request Crosses
  5. The Span Tree as a Diagnostic Tool
  6. Root Cause Analysis Using Traces
  7. Service Maps and Dependency Discovery
  8. Latency Analysis Patterns
  9. Sampling Strategy for Production Systems
  10. Tracing Across Synchronous and Asynchronous Boundaries
  11. Tracing Third-Party and Uninstrumented Dependencies
  12. Trace-Driven Testing and SLOs
  13. Common Pitfalls
  14. Quick Reference Table
  15. Conclusion

Introduction

Distributed tracing is the practice of reconstructing a single logical request's complete journey as it travels across every service, database call, and message it touches in a microservice system — not just observing one service in isolation, but stitching together a coherent, end-to-end picture of what actually happened, in what order, and how long each part took. This guide builds directly on this series' OpenTelemetry guide (which covers the mechanics of spans, trace context, and instrumentation) to focus specifically on distributed tracing as an architectural discipline: why it becomes necessary the moment a system splits into multiple services, and how to actually use traces to diagnose real production problems.

Trace: "Checkout" (poor total latency: 1,840ms)
  ├── API Gateway (5ms)
  ├── OrderService.PlaceOrder (1,820ms)  ← the vast majority of the time is HERE
  │     ├── SQL INSERT (12ms)
  │     ├── gRPC call to InventoryService (45ms)
  │     └── HTTP call to PaymentService (1,740ms)  ← and HERE, specifically
  │           └── HTTP call to external payment gateway (1,710ms)  ← the actual root cause
  └── PublishEvent OrderPlaced (8ms)
Enter fullscreen mode Exit fullscreen mode

Without distributed tracing, diagnosing why checkout felt slow would mean separately checking logs and metrics for the API gateway, OrderService, InventoryService, and PaymentService, and manually correlating timestamps across four different systems to guess at causality. With it, the answer — an external payment gateway call, not your own code — is visible in a single view.


1. The Problem Distributed Tracing Solves

Single-service observability breaks down at the boundary between services

A single service's logs and metrics (covered in this series' Structured Logging guide) tell you everything about what happened within that service — but a request in a microservice architecture rarely stays within one service. As covered in this series' REST, gRPC, and Event-Driven Architecture guides, a single user-facing operation commonly fans out across several services, each with its own logs, its own metrics, and — critically — no inherent way to know it's part of the same larger operation as the other services involved.

The specific questions distributed tracing answers that isolated observability can't

"Why was THIS SPECIFIC checkout slow?" — not "what's our average checkout latency" (a metrics question)
"WHICH service in the chain actually caused the failure?" — not "did Service X have any errors today" (a logs question, per-service)
"What is the ACTUAL dependency chain for this operation, as it happened?" — not "what services do we THINK depend on each other" (architecture documentation, often stale)
Enter fullscreen mode Exit fullscreen mode

Metrics (covered in this series' OpenTelemetry guide) excel at aggregate questions — "how is the system behaving generally" — and logs excel at detailed, single-event questions within one service's context. Distributed tracing exists specifically for the question neither answers well on its own: reconstructing the actual, specific causal chain of one request across every service boundary it crossed.

Why this matters more as microservice count grows

1 service:   the "trace" is just the service's own logs — trivial, no special tooling needed
5 services:   manual timestamp correlation across 5 log streams is tedious but occasionally feasible
30+ services: manual correlation is genuinely impossible; distributed tracing stops being optional
Enter fullscreen mode Exit fullscreen mode

The value of distributed tracing scales directly with the number of services a typical request touches — a monolith or a small handful of services can often get by with careful logging and manual correlation; the microservice architectures covered throughout this series' cloud, containers, and messaging guides genuinely cannot be operated reliably in production without it once the service count and request fan-out grow past a fairly small threshold.


2. Anatomy of a Distributed Trace

Trace, span, and parent-child relationships — the structural foundation

As covered in this series' OpenTelemetry guide, a trace is composed of a tree of spans, each representing one unit of work, linked by parent-child relationships that reconstruct causality — this guide assumes that structural foundation and focuses on what you actually do with it once it's in place.

Root span: "POST /checkout" (API Gateway, 1,840ms)
  └── Child span: "OrderService.PlaceOrder" (1,820ms)
        ├── Child span: "SQL INSERT Orders" (12ms)
        ├── Child span: "gRPC InventoryService.ReserveStock" (45ms)
        └── Child span: "HTTP PaymentService.Charge" (1,740ms)
              └── Child span: "HTTP external-gateway.charge" (1,710ms)
Enter fullscreen mode Exit fullscreen mode

The critical distinction: wall-clock time vs. "this span's own work"

OrderService.PlaceOrder: 1,820ms total
  minus SQL INSERT:            12ms
  minus gRPC to Inventory:     45ms
  minus HTTP to Payment:     1,740ms
  = OrderService's OWN code:    23ms  ← the actual time spent in OrderService itself
Enter fullscreen mode Exit fullscreen mode

A span's total duration includes time spent waiting on its child spans — the genuinely useful diagnostic number is often a span's self time (total duration minus the sum of its children's durations), since that's what tells you whether a specific service's own code is the bottleneck versus whether it's simply waiting on something downstream. Most tracing backends compute and visualize this distinction automatically (often as a "flame graph," Section 4), but it's worth understanding explicitly: a 1,820ms span doesn't mean OrderService itself is slow — in the example above, it's almost entirely waiting on PaymentService.

Span attributes: the difference between "something was slow" and "I know exactly why"

activity?.SetTag("http.method", "POST");
activity?.SetTag("http.status_code", 200);
activity?.SetTag("db.statement", "INSERT INTO Orders ...");
activity?.SetTag("order.id", orderId);
activity?.SetTag("payment.gateway", "stripe");
activity?.SetTag("payment.retry_count", 2);
Enter fullscreen mode Exit fullscreen mode

The attributes attached to a span (covered mechanically in this series' OpenTelemetry guide) are what elevate a trace from "here's a timeline" to "here's a timeline with enough context to actually explain what happened" — a payment.retry_count: 2 tag on a slow payment span, for instance, immediately tells you the slowness likely came from retries against a struggling downstream gateway, not from your own payment service logic being inefficient.

Span events and exceptions

activity?.AddEvent(new ActivityEvent("Retrying payment charge", tags: new ActivityTagsCollection { { "attempt", 2 } }));
activity?.SetStatus(ActivityStatusCode.Error, "Payment gateway timeout");
activity?.RecordException(exception);
Enter fullscreen mode Exit fullscreen mode

Beyond simple start/end timing, a span can carry discrete events (a retry occurring partway through, a cache miss) and can record an exception directly — this is what makes a single trace often sufficient to diagnose a failure without needing to separately cross-reference logs at all, since the actual exception and its stack trace are attached directly to the exact point in the exact span where it occurred.


3. Propagation Across Every Boundary a Request Crosses

Why this is the actual hard part of distributed tracing

The conceptual model (a tree of spans) is simple; the genuinely difficult, detail-heavy work is ensuring trace context survives every single kind of boundary a request might cross in a real microservice system — and missing even one boundary type silently breaks the trace at exactly that point, without any obvious error to alert you it happened.

HTTP and gRPC: the well-trodden path

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
Enter fullscreen mode Exit fullscreen mode

As covered in this series' OpenTelemetry guide, automatic instrumentation for HttpClient, ASP.NET Core, and gRPC clients/servers propagates the W3C Trace Context header transparently — this is the most mature, most reliably automatic propagation path, directly connecting to the synchronous service-to-service call patterns covered in this series' REST and gRPC guides.

Message brokers: requires deliberate, explicit propagation

// As covered in this series' OpenTelemetry and Event-Driven Architecture guides —
// trace context must be explicitly injected into message headers/properties on publish,
// and explicitly extracted on consume, since there's no ambient request context
// automatically flowing across an asynchronous boundary
message.ApplicationProperties["traceparent"] = currentTraceContext;
Enter fullscreen mode Exit fullscreen mode

This is precisely why this series' RabbitMQ, Kafka, and Azure Service Bus guides each explicitly reference message headers/properties as carriers for trace context, and why the Event-Driven Architecture guide emphasizes correlation IDs as a non-negotiable discipline — a trace that silently stops at the boundary into an asynchronous message is a trace that's lost exactly the visibility into "what happened after this event was published" that made distributed tracing worth adopting in the first place.

Background jobs and scheduled tasks

// A BackgroundService (per this series' Background Services guide) processing a queued item
// should start a NEW trace if none was propagated, or continue an EXTRACTED one if it was
using var activity = MyActivitySource.StartActivity("ProcessQueuedItem",
    ActivityKind.Consumer, parentContext: extractedContext);
Enter fullscreen mode Exit fullscreen mode

A BackgroundService-based worker (per this series' Background Services guide) processing work that originated from a traced request needs the same explicit context extraction as a message consumer — and for genuinely scheduled, non-request-originated work (a nightly cleanup job), it's reasonable and correct for the worker to simply start a fresh trace, since there's no meaningful "originating request" for a scheduled job to link back to.

Database calls and caches: usually automatic, worth verifying

tracing.AddEntityFrameworkCoreInstrumentation();
tracing.AddRedisInstrumentation(); // via StackExchange.Redis's own OpenTelemetry support
Enter fullscreen mode Exit fullscreen mode

As covered in this series' OpenTelemetry guide, instrumentation libraries exist for EF Core and common Redis clients, automatically creating child spans for database queries and cache operations within the currently active trace — this "just works" once configured, but it's worth explicitly confirming coverage for whichever specific data access libraries a given service actually uses (Dapper, for instance, per this series' Dapper guide, has less universally standardized automatic instrumentation than EF Core, and may need explicit custom spans wrapped around raw ADO.NET calls).

The practical checklist

For any microservice system, it's worth explicitly auditing every boundary type in use — synchronous HTTP/gRPC calls, every message broker in the architecture, background job processing, database and cache calls — and confirming trace context genuinely propagates across each one, rather than assuming "we have OpenTelemetry configured" automatically covers every boundary type a system happens to use.


4. The Span Tree as a Diagnostic Tool

Flame graphs: the standard visualization

|████████████████████████████████████████████| OrderService.PlaceOrder (1,820ms)
  |██| SQL INSERT (12ms)
  |███| gRPC InventoryService (45ms)
     |████████████████████████████████████████| HTTP PaymentService (1,740ms)
        |███████████████████████████████████| external-gateway.charge (1,710ms)
Enter fullscreen mode Exit fullscreen mode

Most tracing backends (Jaeger, Tempo, Application Insights, Datadog) visualize a trace as a flame graph — horizontal bars representing each span, positioned and sized by their start time and duration, nested to show the parent-child structure. The visual width of a span immediately communicates its relative contribution to total latency — the classic pattern of "one enormous bar dominating the graph" is almost always where the actual investigation should start.

Reading a flame graph for the first time on an unfamiliar trace

The practical workflow: start at the root span (total request duration), visually identify the largest child span (where most of the time actually went), and repeat that process recursively into that child's own children — this quickly narrows an investigation from "checkout was slow" down to "checkout was slow specifically because of this one external payment gateway call," in seconds, without reading a single log line.

Comparing a slow trace against a typical one

Typical trace for this endpoint: 180ms total, PaymentService span: 40ms
This specific slow trace:        1,840ms total, PaymentService span: 1,740ms
Enter fullscreen mode Exit fullscreen mode

The most powerful diagnostic technique isn't examining one slow trace in isolation — it's comparing a specific slow trace's span durations against the typical shape of traces for that same operation (Section 7 covers this more systematically via latency percentile analysis) — a span that's usually fast but occasionally enormous points directly at an intermittent problem (a struggling downstream dependency, a lock contention issue, a retry storm) rather than a consistently slow code path.


5. Root Cause Analysis Using Traces

The workflow: from symptom to root cause

1. Alert fires: p99 latency for /checkout exceeded 1s
2. Find a representative slow trace (via tail-based sampling, Section 9, or a trace search filtered by duration)
3. Identify the dominant span in the flame graph
4. Drill into that span's attributes/events/exceptions for the specific "why"
5. Cross-reference with logs correlated to that exact trace ID, if more detail is needed
Enter fullscreen mode Exit fullscreen mode

This is the concrete workflow distributed tracing enables — starting from an aggregate symptom (a metric-driven alert, per this series' OpenTelemetry guide), finding a specific representative example, and drilling down through the span tree to the actual root cause, rather than starting an investigation from scratch across scattered logs.

Distinguishing "this service is slow" from "this service is waiting on something slow"

Naive read:   "OrderService took 1,820ms — OrderService has a performance problem"
Trace-informed read: "OrderService's OWN code took 23ms — the problem is entirely in PaymentService's
                       downstream call to an external gateway"
Enter fullscreen mode Exit fullscreen mode

This is arguably the single most valuable thing distributed tracing provides that isolated per-service metrics cannot: without a trace, a dashboard showing "OrderService's p99 latency is elevated" would naturally lead an engineer to investigate OrderService's own code — precisely the wrong place to look in this example. The trace redirects the investigation immediately and correctly to PaymentService's external dependency, avoiding a genuinely common and costly wrong-service investigation.

Correlating a trace with logs for maximum detail

Trace shows: PaymentService span failed with status "Error"
→ Query logs filtered to that exact TraceId, per this series' Structured Logging guide
→ Find the specific structured log entry with the full exception details, retry attempts, and gateway response body
Enter fullscreen mode Exit fullscreen mode

As covered in this series' Structured Logging and OpenTelemetry guides, a trace's span-level attributes and events often provide enough detail on their own, but for genuinely complex failures, jumping from a specific span directly to every log entry sharing that trace ID (via the automatic trace/log correlation covered in both guides) gives the fullest possible picture without needing separate, manual log searching.

Root cause analysis across an asynchronous chain

Trace 1 (synchronous, HTTP): "POST /checkout" → OrderService → publishes OrderPlaced event
  [trace 1 ends here — the HTTP response has been returned]

Trace 2 (asynchronous, triggered by the event): InventoryService consumes OrderPlaced → reserves stock
  [a SEPARATE trace, but sharing the same CorrelationId, per this series' Event-Driven Architecture guide]
Enter fullscreen mode Exit fullscreen mode

For genuinely asynchronous, event-driven chains, it's worth being honest that a single OpenTelemetry "trace" often doesn't span the entire business operation the way it does for a purely synchronous request — a message publish frequently ends one trace, and message consumption begins a new one. This is where the correlation ID pattern from this series' Event-Driven Architecture guide remains essential alongside OpenTelemetry tracing, not superseded by it: correlation IDs tie together the (potentially several) distinct traces that together make up one logical, asynchronous business operation, even when OpenTelemetry's own trace boundaries don't cleanly span the whole thing.


6. Service Maps and Dependency Discovery

Traces as the raw material for automatically discovering actual architecture

Service Map (derived automatically from observed trace data):
  API Gateway → OrderService → InventoryService
                             → PaymentService → [external: stripe.com]
                             → (async) OrderPlaced event → EmailService
                                                          → AnalyticsService
Enter fullscreen mode Exit fullscreen mode

Most tracing backends can aggregate many individual traces over time into a service map — a visual graph of which services actually call which other services, and how frequently, derived directly from real, observed trace data rather than from architecture diagrams or documentation, which are notoriously prone to drifting out of sync with what a system has actually evolved into.

Why this matters for genuinely large microservice systems

In a system with dozens of services (per this series' Kubernetes/Helm and Azure/AWS Compute guides), it's common for no single person to have complete, accurate knowledge of every actual dependency — a service map built from real trace data becomes the honest, continuously self-updating source of truth, surfacing dependencies that may have been added months ago by a different team and never formally documented anywhere.

Detecting unexpected or undesirable dependencies

Service map reveals: ReportingService → (unexpectedly) → PaymentService directly
  ← a genuine architectural surprise, worth investigating: should Reporting really call Payment directly?
Enter fullscreen mode Exit fullscreen mode

A service map surfacing a dependency nobody expected — a reporting service calling a payment service directly, say — is a genuinely valuable, unplanned discovery that traces enable almost as a side effect: architectural drift becomes visible and discussable rather than silently accumulating unnoticed.


7. Latency Analysis Patterns

Percentile-based analysis, not just averages

p50 (median) latency for /checkout: 180ms
p95 latency:                          420ms
p99 latency:                        1,840ms
Enter fullscreen mode Exit fullscreen mode

As covered generally in this series' OpenTelemetry guide's metrics discussion, averages hide the shape of a latency distribution — a system with a fast median but a heavy tail of very slow outliers (a common real-world pattern, often caused by exactly the kind of downstream dependency issue from Section 5's example) looks perfectly healthy on an average-latency dashboard while genuinely failing a meaningful fraction of real users. Distributed tracing's specific value here is letting you pull a representative trace from the p99 bucket specifically, rather than only ever examining an "average" trace that, by definition, doesn't actually represent the worst experiences users are having.

The "long tail" investigation pattern

1. Metrics show p99 latency spiked at 2pm
2. Query traces filtered to (a) that time window, and (b) duration > 1000ms
3. Examine several of the slowest matching traces
4. Look for a COMMON pattern across them — same downstream service, same specific operation, same customer segment
Enter fullscreen mode Exit fullscreen mode

Examining several slow traces together (rather than just one) often reveals a pattern a single trace wouldn't — perhaps every slow trace shares a specific downstream call, or a specific customer's requests, or a specific time-of-day correlation with a batch job running concurrently — turning "this one request was slow" into "here's the systemic cause affecting a meaningful class of requests."

Comparing latency across deployments

Trace data before deploying v2.3: p99 = 420ms
Trace data after deploying v2.3:   p99 = 1,840ms  ← the new version introduced a regression
Enter fullscreen mode Exit fullscreen mode

Because traces carry the service version (via the resource metadata covered in this series' OpenTelemetry guide), comparing trace-derived latency distributions immediately before and after a deployment is a direct, evidence-based way to confirm or rule out a specific release as the cause of a latency regression — connecting distributed tracing directly to the deployment strategies and rollback discipline covered in this series' CI/CD Pipelines guide.


8. Sampling Strategy for Production Systems

Why this section exists here too, with a specific microservices lens

As covered in this series' OpenTelemetry guide, sampling controls what fraction of traces are actually captured — worth revisiting here specifically through the lens of "what sampling strategy actually serves distributed tracing's diagnostic goals in a microservice system," since the stakes of losing exactly the wrong trace are higher once dozens of services are involved.

Why uniform random sampling is a poor fit for microservices specifically

Random 1% sampling: a request touching 15 services has its trace captured only if
                     EVERY service along the chain happens to sample it — with independent,
                     uncoordinated sampling decisions per service, this compounds badly
Enter fullscreen mode Exit fullscreen mode

If each service in a chain makes its own independent random sampling decision, the probability that a complete, end-to-end trace survives across many hops compounds multiplicatively and shrinks fast — this is specifically why head-based sampling (deciding once, at the very start of a trace, and propagating that single decision through the traceparent header's sampled flag to every downstream service) is essential for microservices, rather than each service sampling independently.

Consistent, propagated sampling decisions

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
                                                                    └─ sampled flag
Enter fullscreen mode Exit fullscreen mode

The W3C Trace Context header's trailing flag carries the sampling decision made at the trace's origin — every downstream service should honor this propagated decision rather than re-deciding independently, ensuring a sampled trace is complete across every service it touches, not a partial fragment from only some of the services along the way.

Tail-based sampling for guaranteed capture of the traces that matter most

As covered in this series' OpenTelemetry guide, tail-based sampling (buffering a complete trace at the Collector and deciding afterward whether to keep it, favoring errors and high latency) is particularly valuable for microservices specifically because it guarantees the traces most useful for the root-cause-analysis workflow in Section 5 — the ones with errors or unusual latency — are essentially always captured, regardless of the baseline sampling rate applied to routine, healthy traffic.


9. Tracing Across Synchronous and Asynchronous Boundaries

The honest limitation: a single trace doesn't always represent one "business operation"

As touched on in Section 5, a purely event-driven chain (per this series' Event-Driven Architecture guide) commonly produces multiple distinct OpenTelemetry traces — one for the original synchronous request, and separate ones for each asynchronous consumer reacting to a resulting event — rather than one unbroken trace spanning the entire logical business process end to end.

Trace links: connecting related-but-separate traces

var link = new ActivityLink(originalTraceContext);
using var activity = MyActivitySource.StartActivity("ProcessOrderPlacedEvent", links: new[] { link });
Enter fullscreen mode Exit fullscreen mode

OpenTelemetry supports span links specifically for this scenario — explicitly connecting a new trace back to the trace that caused it (the original request that published the event this consumer is now processing), giving tracing backends enough information to visualize the relationship between the two traces even though they're not simply parent and child within a single tree.

When correlation IDs remain the more practical tool

For genuinely long, multi-step, multi-consumer event-driven chains (a saga spanning several services and several asynchronous hops, per this series' Event-Driven Architecture guide), a single shared correlation ID — queryable directly against the centralized structured log store (per this series' Structured Logging guide) — is often the more practical way to reconstruct "everything that happened for this one order," compared to navigating a web of individually-linked traces in a tracing backend's UI. The two tools are complementary: OpenTelemetry traces for the detailed, per-hop timing and causality within any single synchronous or short asynchronous segment, correlation IDs for stitching together the full, potentially long-running, multi-trace story.


10. Tracing Third-Party and Uninstrumented Dependencies

The gap: not everything a request touches emits proper spans

Your traced chain: API Gateway → OrderService → PaymentService
                                                    │
                                                    └── HTTP call to a third-party payment gateway
                                                        (a black box — no span data from INSIDE it)
Enter fullscreen mode Exit fullscreen mode

A trace naturally ends at the boundary of anything your own instrumentation doesn't cover — a third-party API, a legacy system without OpenTelemetry support, a database engine's internal query planning — the span for "call to the external gateway" shows you how long that call took from your side, but nothing about what happened inside it.

Making the boundary itself informative, even without internal visibility

activity?.SetTag("http.url", "https://api.stripe.com/v1/charges");
activity?.SetTag("http.status_code", response.StatusCode);
activity?.SetTag("http.response_content_length", response.Content.Headers.ContentLength);
Enter fullscreen mode Exit fullscreen mode

Even without visibility inside a third-party dependency, capturing rich attributes on the boundary span itself (the exact endpoint called, response status, response size, retry count) — the same automatic HTTP instrumentation covered in this series' OpenTelemetry guide already does much of this — is often sufficient to distinguish "the third party was slow" from "we made an unnecessary number of calls to the third party" or "we're retrying excessively against a struggling third party," without needing internal visibility into the dependency itself.

Synthetic and black-box monitoring as a complement

For genuinely critical third-party dependencies, pairing trace-derived boundary visibility with independent, direct monitoring of that third party (checking its status page, running synthetic checks against its public API) fills in some of what a trace alone can't show — this is a different observability technique than tracing itself, but worth mentioning as the natural complement for the parts of a request's journey that leave your own instrumented system entirely.


11. Trace-Driven Testing and SLOs

Using traces to validate Service Level Objectives

SLO: 99% of checkout requests complete in under 500ms

Query: traces for "POST /checkout" over the last 30 days, compute the actual p99
Result: 99% of requests completed in 480ms — SLO met, with a small margin
Enter fullscreen mode Exit fullscreen mode

Distributed traces provide the raw, ground-truth data for validating Service Level Objectives (SLOs) — rather than a synthetic, periodic health check measuring one specific path, real trace data reflects the actual, full distribution of real user experiences across every code path and dependency combination that occurred, directly connecting to the DORA-metrics and reliability themes covered in this series' CI/CD Pipelines guide.

Trace-informed load and integration testing

Load test scenario derived from real production trace data:
  "Simulate the actual observed distribution of concurrent checkout + inventory-check + payment calls,
   not a synthetic guess at typical traffic patterns"
Enter fullscreen mode Exit fullscreen mode

Real trace data from production is genuinely valuable input for designing realistic load tests — rather than guessing at a plausible traffic pattern, replaying (or statistically modeling) the actual observed mix and timing of calls a system experiences in production produces load tests that stress the system in ways that actually resemble reality, closing a common gap between "our load tests pass" and "we were still surprised by a specific traffic pattern in production."


12. Common Pitfalls

Pitfall Why it hurts Better approach
Assuming OpenTelemetry configuration automatically covers every boundary type Message brokers, background jobs, and some data-access libraries need explicit propagation Audit every boundary type in the architecture explicitly, per Section 3
Independent, uncoordinated sampling decisions per service Complete end-to-end traces become rare across a long chain of services Propagate a single sampling decision from the trace's origin via the traceparent flag
Investigating "this service is slow" without checking self-time vs. child-span time Leads to investigating the wrong service entirely Always distinguish a span's own work from time spent waiting on its children
Treating a single OpenTelemetry trace as always representing one full business operation Asynchronous, event-driven chains commonly span multiple distinct traces Use correlation IDs alongside traces for genuinely long, multi-hop asynchronous chains
Examining only one slow trace in isolation Misses systemic patterns visible only across several slow traces together Compare multiple traces from the same latency bucket to find common causes
No boundary-span attributes for third-party/uninstrumented dependencies A slow external call is visible only as "slow," with no further diagnostic detail Capture rich attributes (URL, status, retry count) even where internal visibility isn't possible
Never using service maps to validate actual vs. documented architecture Architectural drift accumulates silently, undiscovered Periodically review trace-derived service maps against team assumptions

Quick Reference Table

Concept Purpose
Span self time Time spent in a span's own work, excluding its children — the key to locating the real bottleneck
Flame graph Visual representation of a trace's span tree, sized by duration
Root cause analysis workflow Symptom → representative trace → dominant span → attributes/events → correlated logs
Service map Architecture derived automatically from real observed trace data
Percentile-based latency analysis Examining p95/p99 traces specifically, not just averages
Consistent/propagated sampling A single sampling decision honored by every service along a trace, preventing partial traces
Span link Connects two related-but-separate traces (e.g., across a message publish/consume boundary)
Boundary span attributes Rich context on calls to uninstrumented third parties, even without internal visibility
Trace-derived SLO validation Using real trace data as ground truth for reliability targets

Conclusion

Distributed tracing earns its place as a distinct architectural discipline — not just a feature of OpenTelemetry to enable and forget about — the moment a system's request paths genuinely span multiple services, and every microservice pattern covered throughout this series (REST and gRPC calls, event-driven messaging via RabbitMQ/Kafka/Service Bus, background processing) is exactly the kind of boundary-crossing behavior that makes single-service observability insufficient on its own. The payoff is concrete and specific: turning "why was this slow" from a multi-system, manually-correlated guessing exercise into a direct, visual drill-down from symptom to root cause.

Getting real value from it requires the same deliberate discipline this series has emphasized for observability throughout — auditing propagation across every boundary type a system actually uses (not just the easy, automatic HTTP/gRPC ones), sampling consistently rather than independently per service, distinguishing a span's own work from its children's, and pairing traces with correlation IDs for the genuinely asynchronous chains where a single trace's natural boundaries don't span the whole logical operation. Done well, distributed tracing turns a microservice architecture's biggest observability liability — that a request's story is scattered across many independent systems — into its most powerful diagnostic asset.


Found this useful? Feel free to star the repo, open an issue with corrections, or share the flame graph that redirected an investigation to the actual root cause instead of the wrong service.

Top comments (0)