Files
spring-async-demo/async/docs/06-context-propagation.md
Ankur Mhatre 243cccd4ca Add the async module
Every @Async behaviour that surprises people, asserted by a test and captured
to docs/output/: the self-invocation trap, what CGLIB cannot override, the
IllegalArgumentException a plain return type throws, the unbounded queue that
makes max-size decoration, spring.task.execution.propagate-context (new in
Boot 4.1.0), the two Executor beans that leave @Async on an unpooled
SimpleAsyncTaskExecutor, and JEP 491 measured on JDK 21 against JDK 25.
2026-09-01 23:27:47 +05:30

2.7 KiB

prev: Pool sizing · README · next: Which executor runs it

6. spring.task.execution.propagate-context — new in Boot 4.1

Handing a Callable to another thread leaves every ThreadLocal behind. That is why the async thread in a stock application sees no MDC entries, no RequestAttributes, and no SecurityContext — and why "the trace id disappears in the async part" is such a common report.

Spring Boot 4.1.0 added a property for it:

spring:
  task:
    execution:
      propagate-context: true

It decorates the auto-configured executor with ContextPropagatingTaskDecorator, which takes a snapshot of everything registered with micrometer's ContextRegistry at submission time and restores it around the task on the executing thread.

How new is it, exactly

New in 4.1.0. It is absent from TaskExecutionProperties in 4.0.8 and in 3.5.9 — checked by extracting TaskExecutionProperties.class from each spring-boot-autoconfigure jar and running javap over it, rather than by reading release notes. The accessor pair getPropagateContext()/setPropagateContext(boolean) appears first in the 4.1.0 jar.

Note also what it is not: there is no matching spring.task.scheduling.propagate-context. The scheduler is not covered.

Measured

RequestId in this module is a ThreadLocal<String> with a ThreadLocalAccessor registered against ContextRegistry. Two tests differ only in the property:

  • ContextNotPropagatedTest — the async thread reads null.
  • ContextPropagatedTest — the async thread reads req-4711 (output).

The three things that make it silently do nothing

  1. micrometer's context-propagation is not on the classpath. The property is still bound and still accepted; nothing decorates the executor. This module declares io.micrometer:context-propagation explicitly for that reason.
  2. Nothing registered an accessor. The snapshot only carries what ContextRegistry knows about. Libraries that ship accessors (Micrometer tracing, Reactor) register their own; a ThreadLocal of your own does not register itself.
  3. The executor is not the auto-configured one. The decorator is applied by Boot's auto-configuration. Declare your own Executor bean (chapter 7) and you have opted out of the property along with everything else Boot was doing.

SecurityContext propagation is a related but separate mechanism, with its own set of ways to get it wrong; that is covered in Virtual Threads and SecurityContext Propagation.

next: Which executor runs it