A trace context ties related observations together while a request moves through services and background work. An @Async method runs on an executor thread, so thread-local context from the request thread is not automatically present there.
Spring async trace context: follow work across executor threads
Opt in to the intended propagation
Spring Boot can propagate observation context through its auto-configured AsyncTaskExecutor when configured to do so. This carries diagnostic context, not a durable task record. The job still needs bounded admission, explicit failure handling and a plan for process loss. Check which executor your @Async method actually uses; a custom executor may need its own propagation setup.
spring.task.execution.propagate-context=true
management.tracing.sampling.probability=0.10package in.aitrove.receipts;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import java.util.concurrent.CompletableFuture;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
class ReceiptReviewTask {
private final ObservationRegistry observations;
ReceiptReviewTask(ObservationRegistry observations) {
this.observations = observations;
}
@Async
public CompletableFuture<String> review(long receiptId) {
String outcome = Observation.createNotStarted("receipt.review", observations)
.lowCardinalityKeyValue("result", "accepted")
.observe(() -> "reviewed-" + receiptId);
return CompletableFuture.completedFuture(outcome);
}
}The returned Future makes completion and failure observable to the caller. The example’s work is deliberately local; a real review must not claim durable execution merely because the method returned a Future. Sampling reduces exported trace volume, not the cost of the business work. Use bounded values such as result=accepted in low-cardinality tags; receipt IDs belong in controlled logs or trace attributes subject to your privacy policy, not metrics labels.
Common Mistakes
- Assuming @Async preserves every ThreadLocal, including authentication, across threads.
- Dropping the Future and losing the worker exception.
- Putting each receipt ID into a metric tag.
- Calling an in-process async task a recoverable queue.
