Async context

ServiceTalk is a fully asynchronous framework and therefore multiple requests may be multiplexed on the same thread. Also depending on the application’s threading model a single request may be processed on different threads. This means that libraries that rely upon ThreadLocal storage such as MDC would not work as expected. To overcome this limitation, we provide an abstraction called AsyncContext which hooks into the internal async machinery to make sure thread local context data is propagated along with the request.

Although AsyncContext may be convenient to use it shouldn’t be overused when traditional argument passing is an option. The intended use case of AsyncContext is to propagate context across method boundaries which do not accommodate for additional parameters. This is typically applicable for infrastructure tasks like tracing, logging, etc.

AsyncContext is designed to provide a static API to retain state associated across asynchronous boundaries. A static API has a few benefits:

Interop with existing APIs depending on ThreadLocal

Some APIs / features assume static state that is coupled with the current thread. This worked well in the 1-request-per-thread model, however it breaks down when we may process a request on different threads due to asynchronous execution and also share threads for processing different requests. For example the OpenTracing APIs and the MDC APIs assume state is stored in some static structure.

API is decoupled from application data APIs

A use case for AsyncContext is to retain distributed tracing state. This state maybe useful across different APIs and maybe burdensome to account for this state in every API (e.g. gRPC and generated code). It also maybe error prone to rely upon users to propagate this state according to control flow which may result inconsistent state. This state is often optional and not always required, and so if it is baked into the protocol APIs this would make them more complex and not be required.

This maybe seen as a negative in some respects. On the contrary, this does not restrict users from explicitly passing state through their APIs.

However, these benefits don’t come for free. Some costs of the static API are as follows:

Complexity of implementation

ServiceTalk is on the hook for making sure the state is saved/restored across asynchronous boundaries. This means that ReactiveStreams operators, Executors, and the offloading strategy must account for when relying upon AsyncContext. In order to accomplish this ServiceTalk’s asynchronous control flow needs to account for AsyncContext and provide tools for users if they have existing code outside ServiceTalk control flow.


Although asynchronous function composition is not required to use ServiceTalk, the internals of ServiceTalk uses asynchronous function composition as a core abstraction which can be difficult to reason about relative to traditional imperative programming. On top of this, ServiceTalk provides isolation from the EventLoop threads by offloading to application level threads, which introduces asynchronous boundaries. For request/response protocols the goal is for the AsyncContext to be isolated for each request/response, but folks that need to directly interact with AsyncContext need to understand how modifications are visible (or not) relative to where they are set.

Cost of retention

In order to make state available in a static fashion across API calls the mechanism to do this is to use thread local state. The same thread maybe used to process logically independent asynchronous control flows (e.g. EventLoop can process multiple sockets, and each socket can process multiple requests, and offloading may re-use threads to process different requests). This results in frequently accessing the thread local state to save original value, set correct context, and then restore original value.


ServiceTalk provides out-of-the-box support for AsyncContext, details about which can be found here. Users can disable AsyncContext if they do not require this functionality.