Distributed tracing is the only way to understand what’s happening when your request bounces through a dozen different microservices.

Let’s see it in action. Imagine a user trying to place an order. This request might hit:

  1. Frontend Service: Receives the HTTP request.
  2. Auth Service: Verifies the user’s token.
  3. Cart Service: Retrieves items in the user’s cart.
  4. Inventory Service: Checks stock levels.
  5. Order Service: Creates the order record.
  6. Payment Service: Processes the payment.
  7. Notification Service: Sends an order confirmation email.

Without tracing, if the order fails at the Payment Service, you’re left staring at logs from multiple services, trying to piece together the sequence of events and pinpoint the exact failure point.

Distributed tracing instruments your services to propagate a unique trace_id and span_id with each request. When a service receives a request, it creates a new span representing its work, links it to the incoming trace_id, and passes the trace_id and its own span_id (as the parent_span_id) to any downstream services it calls. A tracing backend (like Jaeger or Zipkin) collects these spans and reconstructs the entire request flow as a trace.

Here’s a simplified view of what the data might look like:

// Span from Frontend Service
{
  "trace_id": "a1b2c3d4e5f6",
  "span_id": "001",
  "parent_span_id": null,
  "service_name": "frontend",
  "operation_name": "POST /orders",
  "start_time": "2023-10-27T10:00:00Z",
  "end_time": "2023-10-27T10:00:05Z",
  "tags": {
    "http.method": "POST",
    "http.url": "/orders",
    "http.status_code": 200
  },
  "logs": [
    {"timestamp": "2023-10-27T10:00:01Z", "message": "Calling Auth Service"}
  ]
}

// Span from Auth Service
{
  "trace_id": "a1b2c3d4e5f6",
  "span_id": "002",
  "parent_span_id": "001",
  "service_name": "auth",
  "operation_name": "ValidateToken",
  "start_time": "2023-10-27T10:00:01Z",
  "end_time": "2023-10-27T10:00:01.5Z",
  "tags": {
    "user.id": "user123"
  }
}

// Span from Payment Service (where failure might occur)
{
  "trace_id": "a1b2c3d4e5f6",
  "span_id": "006",
  "parent_span_id": "005",
  "service_name": "payment",
  "operation_name": "ProcessPayment",
  "start_time": "2023-10-27T10:00:03Z",
  "end_time": "2023-10-27T10:00:04Z",
  "tags": {
    "payment.method": "credit_card",
    "payment.status": "failed",
    "error": "Insufficient funds"
  },
  "logs": [
    {"timestamp": "2023-10-27T10:00:03.5Z", "message": "Calling external payment gateway"}
  ]
}

The tracing backend visualizes this as a waterfall, showing the duration of each span and its relationship to others. You can immediately see that the payment service took 1 second, and it failed with "Insufficient funds." You can also see the entire path the request took, the latency introduced by each service, and any errors.

The core problem tracing solves is the visibility gap in distributed systems. When requests hop between services, you lose the single point of control and observation you have with a monolith. Tracing provides this control by making the entire request lifecycle observable. You control which libraries you use for instrumentation (e.g., OpenTelemetry, Jaeger clients) and how much data you send to your tracing backend. The backend then provides the visualization and querying capabilities to understand performance and errors.

The trace_id is the global identifier for a single end-to-end request. The span_id is a unique identifier for a specific operation within that trace (e.g., a single HTTP call, a database query). When service A calls service B, service A’s span becomes the parent, and service B’s span becomes a child, linked via the parent_span_id. This hierarchical structure is what allows the tracing backend to reconstruct the entire request flow.

The most counterintuitive aspect of distributed tracing is that the absence of an error in a service’s own logs doesn’t mean it’s not contributing to a failure. A service might complete its operation successfully, but the latency it introduced, or a malformed response it sent that downstream services couldn’t handle, could be the root cause of a larger system failure. Tracing surfaces these subtle interactions by showing the duration of every span and the exact data passed between services (if logged as tags or events).

The next concept you’ll want to explore is how to correlate traces with logs for even deeper debugging.

Want structured learning?

Take the full Distributed Systems course →