Skip to main content
Distributed tracing connects spans from multiple services into a single trace. dd-trace automatically propagates trace context for supported HTTP clients, gRPC, and messaging integrations. This guide explains how context propagation works and how to implement it manually for custom transports.

How context propagation works

When a service makes an outbound request, dd-trace injects the current trace context into the request as HTTP headers. The receiving service extracts those headers and continues the trace, creating a parent-child relationship between spans across service boundaries.
Service A                         Service B
---------                         ---------
tracer.inject(span, headers)  --> tracer.extract(headers)
     |                                 |
  span A                           span B (child of A)
For supported libraries (e.g., http, fetch, undici, grpc, kafkajs), injection and extraction happen automatically with no code changes required.

Propagation formats

dd-trace supports multiple propagation formats. The format determines which HTTP headers carry the trace context.
FormatHeadersStandard
datadogx-datadog-trace-id, x-datadog-parent-id, x-datadog-sampling-priority, x-datadog-tagsDatadog-specific
tracecontexttraceparent, tracestateW3C TraceContext
b3multix-b3-traceid, x-b3-spanid, x-b3-sampledOpenZipkin B3 multi-header
b3b3 (single header)OpenZipkin B3 single-header

Configuring propagation style

Configure the propagation format using the DD_TRACE_PROPAGATION_STYLE environment variable or the tracePropagationStyle init option.
# Use W3C TraceContext (recommended for interoperability)
DD_TRACE_PROPAGATION_STYLE=tracecontext node app.js

# Use multiple formats (comma-separated)
DD_TRACE_PROPAGATION_STYLE=datadog,tracecontext node app.js
When multiple extraction formats are configured, dd-trace tries each format in order and uses the first one that succeeds.

Manual inject and extract

For custom transports (e.g., message queues, WebSockets, or proprietary protocols), use tracer.inject() and tracer.extract() to propagate context manually.

Injecting context into a carrier

Call tracer.inject() on the sending side to write trace context into a carrier object (typically an HTTP headers map or a message attribute object):
const tracer = require('dd-trace').init()
const opentracing = require('opentracing')

// Producer / client side
function sendMessage(payload) {
  const span = tracer.scope().active()
  const carrier = {}

  if (span) {
    tracer.inject(span.context(), opentracing.FORMAT_HTTP_HEADERS, carrier)
  }

  // carrier now contains the trace headers, e.g.:
  // { 'x-datadog-trace-id': '...', 'x-datadog-parent-id': '...' }
  return queue.send({ payload, headers: carrier })
}

Extracting context from a carrier

Call tracer.extract() on the receiving side to read context from the carrier and use it as the parent of a new span:
const tracer = require('dd-trace').init()
const opentracing = require('opentracing')

// Consumer / server side
function handleMessage(message) {
  const parentContext = tracer.extract(
    opentracing.FORMAT_HTTP_HEADERS,
    message.headers
  )

  tracer.trace(
    'message.process',
    { childOf: parentContext },
    (span) => {
      span.setTag('message.id', message.id)
      processMessage(message)
    }
  )
}

Cross-service HTTP example

The following example shows two Express services connected by distributed tracing. dd-trace handles inject and extract automatically for the built-in http integration. Service A — calls Service B:
// Must be imported before any other modules
require('dd-trace').init({ service: 'service-a' })

const http = require('http')
const express = require('express')
const app = express()

app.get('/orders', (req, res) => {
  // dd-trace automatically injects trace context into outgoing HTTP headers
  const options = {
    hostname: 'service-b',
    port: 3001,
    path: '/inventory',
    method: 'GET'
  }

  const request = http.request(options, (response) => {
    res.json({ status: 'ok' })
  })
  request.end()
})

app.listen(3000)
Service B — receives the request:
// dd-trace automatically extracts trace context from incoming headers
require('dd-trace').init({ service: 'service-b' })

const express = require('express')
const app = express()

app.get('/inventory', (req, res) => {
  // This span is automatically a child of the span from Service A
  res.json({ items: [] })
})

app.listen(3001)

HTTP headers reference

The following headers are used by each propagation format:

Datadog format

HeaderDescription
x-datadog-trace-id64-bit trace ID (decimal string)
x-datadog-parent-id64-bit parent span ID (decimal string)
x-datadog-sampling-prioritySampling decision: 1 (keep) or 0 (drop)
x-datadog-tagsPropagated tags (e.g., _dd.p.dm=-1)

W3C TraceContext format

HeaderDescription
traceparentVersion, trace ID, parent ID, and flags (e.g., 00-<trace-id>-<parent-id>-01)
tracestateVendor-specific state, including Datadog sampling info

B3 multi-header format

HeaderDescription
x-b3-traceid64 or 128-bit trace ID (hex string)
x-b3-spanid64-bit span ID (hex string)
x-b3-sampled1 (sampled) or 0 (not sampled)
When using W3C TraceContext (tracecontext) format, dd-trace uses 128-bit trace IDs. The datadog format uses 64-bit trace IDs by default.

Build docs developers (and LLMs) love