A New Collection of Thoughtful Learning Apps — Now Available on iOS & Android

Image
I’m excited to share a set of mobile apps I’ve recently completed and published on both the Google Play Store and the Apple App Store. These apps are designed with a simple goal in mind: to make meaningful, structured content more accessible, whether you’re studying theology or improving your English vocabulary. 📱 Now Available on Both Platforms All apps are live and available for download: Google Play Developer Page: https://play.google.com/store/apps/dev?id=5835943159853189043 Apple App Store Developer Page: https://apps.apple.com/ca/developer/q-z-l-corp/id1888794100 📖 Theology & Confession Study Apps For those interested in Reformed theology and classical Christian teachings, I’ve developed a series of apps that present foundational texts in a clean, focused reading format: The Belgic Confession Canons of Dort Heidelberg Catechism Westminster Shorter Catechism Each app is designed to provide a distraction-free experience, making it easier to read, reflect, and revisit these im...

Spring Boot Observability: Propagating Trace Context Across Async Threads

Spring Boot @Async: Fixing Distributed Tracing Context Propagation Across Threads



Have you ever noticed that distributed traces suddenly disappear when your Spring Boot application executes an asynchronous task?

A request enters your application, calls a service, and then schedules background work using @Async. The initial request appears correctly in your tracing dashboard, but the background execution is missing—or worse, it appears as an entirely unrelated trace.

The problem may not be your tracing backend. It may be that the request's observation context was not propagated to the worker thread.

Distributed tracing relies on context: identifiers and other state that connect operations across services and execution boundaries. OpenTelemetry provides mechanisms to propagate this context between services, usually without requiring application code to copy identifiers manually.

However, there is another boundary within the same application that is easy to overlook: switching threads.

When Spring executes an @Async method on a different thread, the original thread's context does not automatically follow it. Without proper context propagation, the asynchronous operation loses its relationship with the original request.

Let's look at why this happens and how to fix it.

1. Understanding Two Different Context Propagation Boundaries

There are two common scenarios in distributed tracing, and they require different solutions.

Service-to-service communication

When one service makes an HTTP request to another service, tracing instrumentation typically injects the current trace context into HTTP headers.

OpenTelemetry's default propagator uses the W3C Trace Context format.

The traceparent header carries three important pieces of information:

  • Trace ID: Identifies the overall distributed trace.

  • Parent span ID: Identifies the operation that initiated the next operation.

  • Trace flags: Carry tracing-related flags, such as the sampling decision.

The receiving service extracts this information and continues the existing trace.

Asynchronous execution within the same application

The situation is different when Spring Boot switches execution from the request thread to a worker thread.

Java applications commonly use ThreadLocal to store contextual information associated with the current thread.

However, a worker thread executing an @Async method does not automatically inherit the request thread's ThreadLocalvalues.

This means the trace context can be lost even though the asynchronous task is part of the same application and was triggered by the original request.

Spring Boot provides context propagation support to address this problem.

Its observability documentation specifically covers context propagation across threads and reactive pipelines, including an opt-in configuration for @Async.

2. Enable Context Propagation for @Async

If your asynchronous methods use Spring Boot's auto-configured AsyncTaskExecutor, you can enable context propagation through application configuration.

Add the following property to application.yml:

spring:
  task:
    execution:
      propagate-context: true

This tells Spring Boot to propagate the current observation context when executing tasks through its auto-configured executor.

With this configuration, tracing context can follow asynchronous tasks instead of being lost at the thread boundary.

What if You Use a Custom Executor?

Many applications define their own AsyncTaskExecutor using ThreadPoolTaskExecutor to control thread pool size, queue capacity, naming, and other execution settings.

In that case, enabling the auto-configured executor property is not sufficient.

You need to configure a ContextPropagatingTaskDecorator for your custom executor.

For example, first register the decorator as a Spring bean:

@Bean
ContextPropagatingTaskDecorator contextPropagatingTaskDecorator() {
    return new ContextPropagatingTaskDecorator();
}

Then apply it to your custom executor:

@Bean
ThreadPoolTaskExecutor taskExecutor(
        ContextPropagatingTaskDecorator decorator) {

    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

    executor.setCorePoolSize(5);
    executor.setMaxPoolSize(10);
    executor.setQueueCapacity(100);

    executor.setTaskDecorator(decorator);

    return executor;
}

The important part is:

executor.setTaskDecorator(decorator);

This ensures that the task decorator captures and restores the relevant context when the task moves between threads.

Choose the configuration that matches your executor. The auto-configured executor property does not automatically configure a separately constructed executor.

3. Context Propagation Does Not Automatically Create a Span

There is another important distinction.

Propagating context and creating a span are two different things.

Context propagation allows an instrumented operation to continue the existing trace. However, it does not guarantee that every method annotated with @Async automatically creates a new, visible span.

If you want a specific background operation to appear as a named span, you need to instrument that operation.

Spring Boot uses Micrometer Observation as the foundation for its metrics and tracing integration.

For example:

Observation.createNotStarted(
        "report.generate",
        observationRegistry
).observe(() -> generateReport());

This creates an observation around the report-generation operation.

When tracing is enabled and properly configured, this observation can produce a span associated with the current trace context.

Be Careful with Metric Tags

Micrometer Observation supports two types of key-values:

  • Low-cardinality values: Added to both metrics and traces.

  • High-cardinality values: Added to traces only.

This distinction matters when designing observability for production applications.

For example, avoid using unique identifiers such as user IDs, request IDs, or order IDs as metric dimensions.

Doing so can create excessive metric cardinality and significantly increase monitoring costs.

Keep metric tags bounded and use trace attributes when you need detailed, per-request information.

4. A Practical Troubleshooting Checklist

If your asynchronous operations are missing from distributed traces, work through the following checks.

Step 1: Identify the last visible span

Find the last visible span in your tracing dashboard.

Note its trace ID and parent span ID. This gives you a reference point for investigating where the trace context disappears.

Step 2: Check which executor is being used

Determine whether your @Async method uses:

  • Spring Boot's auto-configured AsyncTaskExecutor.

  • A custom ThreadPoolTaskExecutor.

  • Another custom executor.

Make sure you configure context propagation on the executor that actually executes the task.

Step 3: Enable context propagation

For the auto-configured executor, use:

spring:
  task:
    execution:
      propagate-context: true

For a custom executor, register and apply ContextPropagatingTaskDecorator.

Step 4: Verify that the background operation is instrumented

Context propagation alone does not guarantee a visible span.

If you expect a dedicated span for the asynchronous operation, make sure it is instrumented using Micrometer Observation or another appropriate tracing mechanism.

Step 5: Check the resulting trace

Trigger the same request again.

Verify that:

  • The background span has the original trace ID.

  • The parent-child relationship makes sense.

  • The span is successfully exported.

  • Sampling, filtering, or dashboard configuration is not hiding the span.

5. What About Message Brokers and Separate Processes?

The solutions described above apply to asynchronous execution within the same application process.

If your background work crosses a message broker or starts in another process, a local task decorator cannot propagate context across that boundary.

For example, if a Spring Boot application publishes a message to Kafka and another service consumes it, tracing instrumentation must inject context into the message headers and extract it on the consumer side.

Use the appropriate tracing instrumentation for the transport rather than relying on ThreadLocal propagation.

Also, remember that trace IDs are identifiers, not authorization tokens. Avoid putting credentials or personal information into propagated baggage.

Conclusion

The key to troubleshooting missing Spring Boot traces is understanding where context propagation is required.

There are two different boundaries to consider:

  • Network boundary: traceparent and tracing instrumentation connect operations across services.

  • Execution boundary: Context propagation carries the active observation across local thread switches and asynchronous execution.

For Spring Boot's auto-configured @Async executor, enabling spring.task.execution.propagate-context is the straightforward solution.

For custom executors, use ContextPropagatingTaskDecorator.

Finally, remember that propagating context does not automatically create a span. If you want background operations to appear as meaningful spans, make sure those operations are instrumented.

Once context propagation and instrumentation are both handled correctly, asynchronous work can be investigated as part of the original request instead of appearing as a mystery of its own.

References

❤️ Support This Blog


If this post helped you, you can support my writing with a small donation. Thank you for reading.


Comments

Popular Posts

A New Collection of Thoughtful Learning Apps — Now Available on iOS & Android

react-native run-android : do not build/update modified code(App.js)