Backend Development 7 min read

Understanding Web3j Asynchronous Calls and JVM Exit Issues

The article analyzes why using Web3j's sendAsync() method can prevent the JVM from terminating, examines the underlying thread‑pool implementation in org.web3j.utils.Async, and offers debugging steps and possible work‑arounds for developers.

FunTester
FunTester
FunTester
Understanding Web3j Asynchronous Calls and JVM Exit Issues

While learning Web3j, the author discovered that invoking sendAsync() caused the JVM process to stay alive. Initial suspicion fell on user code and the FunTester framework, but isolating Web3j revealed the problem originated from its asynchronous API.

Web3j implements async callbacks through three layers: the public sendAsync() method, an overridden method that delegates to Async.run() , and finally the static Async.run() method which creates a CompletableFuture using a cached thread pool executor.

The executor is defined as a static field in org.web3j.utils.Async :

private static final ExecutorService executor = Executors.newCachedThreadPool();

This creates a cached thread pool with an unbounded maximum size and a keep‑alive time of 60 seconds. Because Web3j does not expose a direct shutdown API for this executor, it registers a JVM shutdown hook that calls Async.shutdown(executor) when the JVM exits.

When the JVM attempts to shut down, the hook can only run after the cached thread pool’s idle threads have timed out (60 s). If any thread remains in TIMED_WAITING on the thread‑pool’s internal SynchronousQueue , the shutdown hook never executes, leaving the JVM hanging.

Using tools like jvisualvm or jconsole, the author identified a thread named pool-4-thread-1 stuck in TIMED_WAITING , confirming that the thread belongs to Async.executor and is the root cause of the JVM not exiting.

Inspection of ThreadPoolExecutor#getTask() shows that the work queue’s poll() call respects the keepAliveTime (60 s), which explains the delayed shutdown.

A minimal reproducible case demonstrates the issue by creating a cached thread pool, registering a shutdown hook, submitting a task, and then waiting on a CompletableFuture . The JVM does not terminate until the 60‑second keep‑alive expires.

Two mitigation strategies are suggested: (1) avoid using Web3j’s built‑in async features and implement custom asynchronous handling, or (2) use reflection to shut down the internal ThreadPoolExecutor directly. The author does not provide concrete code for the second approach.

JavaJVMthreadpoolasynchronousWeb3j
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.