All Products
Search
Document Center

Application Real-Time Monitoring Service:Pass trace context across asynchronous boundaries in Java

Last Updated:Mar 10, 2026

When threads or processes communicate through intermediaries such as message queues or databases, the ARMS agent cannot automatically propagate trace context. The consumer starts a new, disconnected trace instead of continuing the producer's trace. Use the OpenTelemetry SDK for Java to manually capture and restore trace context across these async boundaries, keeping all spans correlated in a single trace.

When you need manual propagation

The ARMS agent automatically propagates trace context for the following async patterns without code changes:

  • Thread pools created with JDK, Spring, or Netty

  • Reactive programming with Reactor or RxJava

  • Spring @Async annotations

  • Reactive frameworks such as Spring Webflux and Spring Gateway

For the full list of supported frameworks, see Java components and frameworks supported by ARMS.

Manual propagation is required when threads or processes communicate through intermediaries that the ARMS agent does not instrument. Two common examples:

ScenarioDescription
Producer-consumer queueThread T1 places a message in a queue, and thread T2 retrieves it for processing. Without manual propagation, T2 starts a new trace instead of continuing T1's trace.
Database-mediated handoffProcess P1 writes a batch of records to a database, and process P2 reads and processes them. Without manual propagation, the processing trace is disconnected from the write trace.

Prerequisites

Before you begin, make sure that you have:

Add dependencies

Add the following Maven dependencies to introduce OpenTelemetry SDK for Java. For more information, see Instrumentation.

<dependencies>
    <dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-api</artifactId>
    </dependency>
    <dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-sdk-trace</artifactId>
    </dependency>
    <dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-sdk</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.opentelemetry</groupId>
      <artifactId>opentelemetry-bom</artifactId>
      <version>1.23.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Pass context across async boundaries

The pattern has two parts:

  1. Capture: On the sender side, call Context.current() to capture the active trace context and store it alongside the data being passed.

  2. Restore: On the receiver side, call context.makeCurrent() to restore the captured context before processing.

Example: Producer-consumer with a blocking queue

The following code passes trace context through a LinkedBlockingQueue. The producer captures the current context when enqueueing a message, and the consumer restores it before processing.

import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import java.util.concurrent.LinkedBlockingQueue;

// Wrap both the message and its trace context
class Event {
    private final Context context;
    private final String msg;

    public Event(Context context, String msg) {
        this.context = context;
        this.msg = msg;
    }

    public Context getContext() {
        return context;
    }

    public String getMsg() {
        return msg;
    }
}

// Shared queue between producer and consumer threads
private final LinkedBlockingQueue<Event> queue = new LinkedBlockingQueue<>();

// Producer: capture the current trace context
public void produce(String msg) {
    queue.add(new Event(Context.current(), msg));
}

// Consumer: restore the trace context before processing
public void consume() throws InterruptedException {
    Event event = queue.take();
    // makeCurrent() sets the captured context as the active context.
    // The try-with-resources block ensures the context is cleaned up after processing.
    try (Scope scope = event.getContext().makeCurrent()) {
        processEvent(event);
    }
}

public void processEvent(Event event) {
    // Any spans created here automatically become children
    // of the producer's trace.
}

Key points

ConceptDescription
Context.current()Captures the active trace context at the time of the call. Store it alongside the data being passed between threads.
context.makeCurrent()Restores the captured context on the consumer thread. Spans, logs, and metrics created within the try (Scope scope = ...) block are correlated with the original trace.
ScopeImplements AutoCloseable. Always use a try-with-resources block to make sure the context is properly cleaned up.

What to do next

Associate trace IDs with application logs to streamline error analysis. See Associate trace IDs with logs for a Java application.