# TransmittableThreadLocal: passing ThreadLocal values into pooled threads

> Alibaba's TransmittableThreadLocal is a zero-dependency Java library that carries ThreadLocal values from the thread that submits a task to the pooled thread that runs it. Here is how the mechanism works, how to wire it into a thread pool, and where it stops being the right tool.

**alibaba/transmittable-thread-local** — 📌 a missing Java std lib(simple & 0-dependency) for framework/middleware, provide an enhanced InheritableThreadLocal that transmits values between threads even using thread pooling components.

- Repository: https://github.com/alibaba/transmittable-thread-local
- Website: https://github.com/alibaba/transmittable-thread-local
- Stars: 8,309 · Forks: 1,725
- Language: Java
- License: Apache-2.0
- Published: 2026-09-22 · Updated: 2026-09-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/alibaba-transmittable-thread-local

## The gap between InheritableThreadLocal and a pooled executor

InheritableThreadLocal copies a value from a parent thread to a child thread at the moment the child is created. That works for a thread you spawn yourself. It does nothing useful for a thread pool, because the pool creates its worker threads once and then reuses them for unrelated tasks. The parent-child relationship that InheritableThreadLocal relies on is established at pool construction, not at task submission.

The README frames the problem in exactly these terms: what an application actually needs is to move the ThreadLocal value present when a task is submitted to the thread pool over to the moment that task executes. Those are two different points in time and, with a pool, two different threads with no parent-child link. TransmittableThreadLocal extends InheritableThreadLocal and adds the second half of the job.

The intended audience is framework and middleware developers rather than application authors. The README lists distributed tracing and full-link stress testing, log collection with contextual fields, request-scoped caches, and application containers or upper-layer frameworks passing information down to an SDK. In all four, the value is set on the request thread and consumed somewhere deeper, often after a queue hop.

## Capture, replay, restore: the three-step mechanism

The library does not make a thread pool inherit anything. It intercepts the task. When a Runnable or Callable is submitted, the wrapper captures a snapshot of the TransmittableThreadLocal values held by the submitting thread. When the task runs on a pool worker, the wrapper installs that snapshot into the worker thread, executes the task, and then restores the worker thread's previous state. The restore step is what makes the design safe for a pool: the worker is handed back to the pool in the state it was borrowed in, so the next task does not see the previous task's context.

The README documents three ways to apply this interception. You can wrap individual Runnable and Callable instances. You can wrap the thread pool itself, which is the option that covers every submission made through that pool. Or you can attach a Java agent that modifies the JDK thread pool implementation classes, which means you do not have to touch the call sites at all.

The README states that the core of the library, counting user APIs, the ExecutorService, ForkJoinPool and TimerTask wrappers and their thread factories, plus the developer and integration APIs, comes to roughly 1000 SLOC. That matters for review: this is a small enough surface to read before you put it in a production classpath. The README also describes it as zero-dependency, which means it does not drag a transitive graph behind it. The repository is split into ttl-core, ttl-agent, ttl-bom, ttl-integrations, ttl-kotlin and ttl2-compatible modules.

## Adding the Maven dependency and wrapping a pool

The README points to Maven Central for the artifact, and the badge links the coordinates under the group com.alibaba. The README's Maven dependency section is the canonical place to look for the version, so take it from there rather than from a blog post; the README gives the groupId as com.alibaba and the artifactId as transmittable-thread-local, and does not pin a version inline.

With the dependency in place, the second step is the pool. The README's user guide section 2.2 is titled around decorating the thread pool, and that is the approach to prefer when you control the pool's construction: wrap it once and the submissions made through that reference are covered. The README names the wrapper class TtlExecutors, which returns a wrapped executor service that you use in place of the original reference.

After wrapping, a value set with TransmittableThreadLocal on the submitting thread is visible inside the task body even though the task runs on a reused worker. The README's section 2.1 covers the alternative for when you cannot replace the pool reference: wrapping the Runnable and Callable themselves. Section 2.3 covers the Java agent, which decorates the JDK thread pool implementation classes so that existing code needs no change. The README documents the agent's startup parameters under that section, so check there for the argument format before adding it to a JVM command line.

## Where the wrapper approach breaks down

The pool-wrapping route only helps if the pool is actually wrapped. A thread pool created somewhere else, a pool obtained from a framework's internal factory, or a submission made directly against an unwrapped reference will silently lose the context, and it will lose it at runtime rather than at compile time. Nothing in the type system stops you from holding both a raw ExecutorService and its TtlExecutors-wrapped twin. That is the failure mode to design against: the value is simply absent in the task, and the symptom shows up as a missing trace ID in a log line, far from the code that caused it.

The agent route removes the call-site discipline problem but adds a different one. It modifies JDK classes at load time, which is a heavier thing to explain to a team that owns the JVM arguments, and it interacts with whatever else is already instrumenting the same classes. The README presents it as an option, not as the default, and that ordering is worth respecting.

There is also a cost question the README does not quantify. Capture and replay happen on every submission, so a pool that handles a very high rate of tiny tasks pays for the snapshot each time. The library is small, but it is not free at the call site. If your tasks are sub-microsecond and your context map is large, measure before assuming the wrapper is invisible.

## Branch and version: read the README banner first

The default branch is not the version most teams should be running. The README carries an explicit note at the top: the master branch holds TransmittableThreadLocal v3, which is described as in development and not yet released, with the version notes and work items tracked in issue 432. The stable release line in use, v2.x, lives on the 2.x branch.

The most recent releases in the repository are v2.14.5 from 2023-12-25, v2.14.4 from 2023-12-02, and v2.14.3 from 2023-07-05. The repository itself is not archived and the last push was on 2026-06-18, but the release tags tell a different story from the commit activity: the published artifacts have not moved since December 2023. Treat v2.14.5 as the current stable artifact and do not expect the master branch to be a drop-in.

Java baseline is version-dependent. The README states that from v2.13 onward the library requires Java 8, and that Java 6 support means staying on 2.12.x. The badge on the README advertises Java 6+ support, which is true of the project's history rather than of every artifact. Check the artifact you pin against your runtime.

## TransmittableThreadLocal against plain InheritableThreadLocal and Micrometer

The closest comparison is with InheritableThreadLocal itself, and the difference is structural rather than a matter of degree. InheritableThreadLocal copies at thread creation and never again; TransmittableThreadLocal copies at submission and restores after execution. If your work is dispatched to a freshly created thread per task, InheritableThreadLocal is already sufficient and the extra library earns nothing. If your work goes through a pool, InheritableThreadLocal gives you the value that was present when the pool was built, which is usually the wrong value or null.

Micrometer's context propagation module solves an overlapping problem from the observability side. It is built around its own context abstraction and its own registry, and it propagates that context rather than arbitrary ThreadLocal values. TransmittableThreadLocal is context-agnostic: it transmits whatever you stored in a TransmittableThreadLocal, which is why the README positions it for tracing systems, log collection, request-scoped caches and SDK handoff alike. If you already run Micrometer and only need trace context to reach a pool, its propagation is the narrower fit. If you need a request-scoped cache or a custom tenant field to cross the same boundary, TransmittableThreadLocal is the more general mechanism, and it comes with no registry to configure.

## Licence, upgrade cost and what to verify

The project is licensed under Apache-2.0, and the LICENSE file sits at the repository root. Apache-2.0 permits commercial and closed-source use and includes a patent grant; it also requires that you retain the licence and notice files for the portions you redistribute. That is a general description of the licence text, not advice about your situation, and if you are redistributing a modified copy you should read the LICENSE file and, where the stakes are high, take your own counsel.

Upgrade cost is low in the ordinary case and awkward at the boundaries. The library is zero-dependency, so a version bump does not cascade through a dependency graph. The two boundaries that do bite are the Java baseline, which moved to Java 8 at v2.13, and the v3 work on master, which the README says is unreleased. A team on Java 6 must stay on 2.12.x; a team tempted by the master branch is reading code that the README itself labels as in development.

What to verify before adopting: that every pool whose tasks need context is wrapped, that the artifact you pin is a 2.x release rather than a master snapshot, and that the JVM argument format for the agent matches what the README's agent section documents. The first of those is the one that will cost you an afternoon of debugging a missing trace ID.

## Conclusion

Adopt TransmittableThreadLocal when your tracing, logging or request-scoped cache context has to survive a handoff to an ExecutorService, ForkJoinPool or TimerTask, and you want that without adding a dependency. Do not adopt it if your code never pools threads, or if you cannot accept that every task submission pays for capture and replay. Before rolling it out, check that your pool is wrapped (or that the agent covers it), that the TtlRunnable and TtlCallable wrappers are applied at every submission site, and that the release you pin is the one whose Java baseline matches your runtime: the README states that v2.13 and later require Java 8, while 2.12.x is the line for Java 6.

## FAQ

### What does TransmittableThreadLocal do that Java's ThreadLocal does not?

ThreadLocal stores a value per thread with no transfer at all, and InheritableThreadLocal transfers it once, when a child thread is created. TransmittableThreadLocal transfers the value from the thread that submits a task to the pooled thread that executes it, which is the case InheritableThreadLocal cannot cover.

### What Maven coordinates does TransmittableThreadLocal use?

The README's Maven dependency section gives the group as com.alibaba and the artifact as transmittable-thread-local, with Maven Central linked from the release badge. The README points at that section for the version rather than naming one inline.

### Which Java versions does TransmittableThreadLocal support?

The README states that from v2.13 onward the library requires Java 8, and that Java 6 support means using the 2.12.x line. The README badge advertises Java 6+ support across the project's history.

### Is TransmittableThreadLocal thread-safe?

The library is built for threads and thread pools, and its wrapper restores a worker thread's previous state after each task so the next task does not inherit the previous one's context. The README describes the wrapper APIs and the agent, not a concurrency guarantee in those words, so the safe reading is that the wrappers are the mechanism you rely on.

## Sources

- [alibaba/transmittable-thread-local on GitHub](https://github.com/alibaba/transmittable-thread-local)
- [License: Apache-2.0](https://github.com/alibaba/transmittable-thread-local/blob/master/LICENSE)
- [Project website](https://github.com/alibaba/transmittable-thread-local)
- [README](https://github.com/alibaba/transmittable-thread-local/blob/master/README.md)
- [Releases](https://github.com/alibaba/transmittable-thread-local/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/alibaba-transmittable-thread-local
