1. Overview

In this article, we’ll explore the warning: “Possibly blocking call in non-blocking context could lead to thread starvation”. First, we’ll recreate the warning with a simple example and explore how to suppress it if it’s not relevant to our case.

Then, we’ll discuss the risks of ignoring it and explore two ways to address the issue effectively.

2. Blocking Method in Non-Blocking Context

IntelliJ IDEA will prompt the “Possibly blocking call in non-blocking context could lead to thread starvation” warning if we try to use a blocking operation in a reactive context.

Let’s assume we’re developing a reactive web application using Spring WebFlux with a Netty server. We’ll encounter this warning if we introduce blocking operations while handling HTTP requests that should remain non-blocking:

intellij warning

This warning originates from IntelliJ IDEA’s static analysis. If we are confident it won’t impact our application, we can easily suppress the warning using the “BlockingMethodInNonBlockingContext” inspection name:

@SuppressWarnings("BlockingMethodInNonBlockingContext")
@GetMapping("/warning")
Mono<String> warning() { 
    // ...
 }

However, it’s crucial to understand the underlying issue and assess its impact. In some cases, this can lead to blocking the threads responsible for handling HTTP requests, causing serious implications.

3. Understanding the Warning

Let’s showcase a scenario where ignoring this warning can lead to thread starvation and block incoming HTTP traffic. For this example, we’ll add another endpoint and intentionally block the thread for two seconds using Thread.sleep(), despite being in a reactive context:

@GetMapping("/blocking")
Mono<String> getBlocking() {
    return Mono.fromCallable(() -> {
        try {
            Thread.sleep(2_000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return "foo";
    });
}

In this case, Netty’s event loop threads handling incoming HTTP requests can become quickly blocked, leading to unresponsiveness. For example, if we send two hundred concurrent requests, the application will take 32 seconds to respond to all of them, even though no computation is involved. Furthermore, this will affect other endpoints as well – even if they don’t require the blocking operation.

This delay occurs because the Netty HTTP thread pool has a size of twelve, so it can only handle twelve requests at a time. If we check the IntelliJ profiler, we can expect to see the threads being blocked most of the time and a very low CPU usage throughout the test:

netty blocking calls

4. Fixing the Issue

Ideally, we should switch to a reactive API to resolve this issue. However, when that’s not feasible, we should use a separate thread pool for such operations to avoid blocking the HTTP threads.

4.1. Using a Reactive Alternative

First, we should aim for a reactive approach whenever possible. This means finding reactive alternatives to blocking operations.

For example, we can try to use reactive database drivers with Spring Data Reactive Repositories or reactive HTTP clients like WebClient. In our simple case, we can use Mono’s API to delay the response by two seconds, instead of relying on the blocking Thread.sleep():

@GetMapping("/non-blocking")
Mono<String> getNonBlocking() {
    return Mono.just("bar")
      .delayElement(Duration.ofSeconds(2));
}

With this approach, the application can handle hundreds of concurrent requests and send all responses after the two-second delay we’ve introduced.

4.2. Using a Dedicated Scheduler for the Blocking Operations

On the other hand, there are situations where we don’t have the option to use a reactive API. A common scenario is when querying a database using a non-reactive driver, which will lead to blocking operations:

@GetMapping("/blocking-with-scheduler")
Mono<String> getBlockingWithDedicatedScheduler() {
    String data = fetchDataBlocking();
    return Mono.just("retrieved data: " + data);
}

In these cases, we can wrap the blocking operation in a Mono and use subscribeOn() to specify the scheduler for its execution. This gives us a Mono that can later be mapped to our desired response format:

@GetMapping("/blocking-with-scheduler")
Mono<String> getBlockingWithDedicatedScheduler() {
    return Mono.fromCallable(this::fetchDataBlocking)
      .subscribeOn(Schedulers.boundedElastic())
      .map(data -> "retrieved data: " + data);
}

5. Conclusion

In this tutorial, we covered the “Possibly blocking call in non-blocking context could lead to thread starvation” warning generated by IntelliJ’s static analyzer. Through code examples, we demonstrated how ignoring this warning can block Netty’s thread pool for handling incoming HTTP requests making the application unresponsive.

After that, we saw how favoring reactive APIs wherever possible can help us solve this issue. Additionally, we learned that we should use a separate thread pool for the blocking operations whenever we have no reactive alternatives.

As always, the source code for this tutorial is available over on GitHub.