Block Image

With the release of Spring Boot 3.2, support for virtual threads has officially been added!
So I thought I would run load tests on two Spring Boot applications, one using the reactive paradigm, the other using precisely virtual threads. I created two simple applications that perform common operations; data retrieval from a database and REST call to an external service.

Specifically, the two applications have the following REST API in GET:
/users?name=${name}&surname=${surname}.
This API:

  1. performs the following query on a Postgres table:
    SELECT * FROM USERS WHERE NAME=${name} AND SURNAME=${surname}
  2. for each resulting row of the query (which in practice will always be one), a REST call is made to an HTTP client that given the surname as input, returns the latter in uppercase. This HTTP service responds in about 300 milliseconds
  3. finally a list of name,surname pairs with the surname in uppercase is returned to the client.

Two lines about WebFlux

Spring WebFlux uses the Reactor library, an implementation of Reactive Streams. This API makes it possible to create non-blocking applications. Instead of using the classic "thread per request" paradigm, it uses that of "event-loop" so dear to Javascript. With this approach it is possible to manage computation resources with more optimization, since we do not have threads "waiting".
The reactive approach involves a type of functional programming, which for those coming from the world of pure Java, is not easy to digest.

Two lines about Virtual Threads

Virtual threads are officially available with Java version 21, thanks to the Loom project.
In summary, prior to this feature, Java threads were associated 1:1 with operating system threads.
Creating threads in Java therefore was wasteful, as was keeping them stuck waiting for something!

With Virtual Threads this is no longer true! Virtual Threads are always instances of java.lang.Thread that are, however, not tied 1:1 to operating system threads. When the code of a virtual thread encounters a blocking operation, the JVM will suspend that virtual thread until the result of the operation is available. The operating system thread associated to that virtual thread is "freed" for use by another virtual thread.
Virtual threads are lightweight objects; we can create thousands of them.

They are suitable for applications that have many blocking operations. They are not suitable for CPU-intensive applications.
Also, with the fact that we can create thousands of threads, it might be risky to abuse them when we want to store something in the context of threads (think about the use of ThreadLocal).

Test preparation data

  • Machine used: AWS t3.micro (2 vCPU, 1 GiB RAM) with Amazon Linux 2023 operating system.
  • Database: Postgres table with about 1800 records.
  • Network: the server application (the WebFlux one and the "virtual thread" one), mock-client application and Postgres database on the same VPC.
  • HTTP Client used: WebClient for WebFlux, RestClient for virtual thread.
  • Database connection drivers: R2DBC for WebFlux (with Spring Data R2DBC), JDBC for "virtual thread" (with Spring Data JDBC).
  • Spring Boot Version: 3.2.0
  • Applications Code: spring-virtual-thread-test
  • Tool used for load testing: jMeter

For test results, the Throughput parameter will be taken into account (you can find the parameter definition here) and the average response time, expressed in milliseconds.
The higher the Throughput value, the better.
The lower the value of Average Response Time, the better.

The test uses random values of name and surname (values, however, present in the Database) for each HTTP call made.

In each test, for each type of parameter, the winner will be colored green, the loser red. They will both be colored gray if there is no winner.

Update: Graphs of the CPU and Heap used were also added. Since these are two different values of the same graph, I have not decreed a winner for each test. However, at the end of the article I will provide my thoughts on these two aspects as well.

First load test: 100 concurrent users, repeated 20 times

  • Number of Threads (users): 100.
  • Loop Count: 20.
  • Total requests: 2000.

Average Response Time: WebFlux 351 - VT 350 (no winner)

Block Image
WebFlux
Block Image
Virtual Threads

Throughput: WebFlux 270.4 - VT 258.2 (+ 12.2 for WebFlux)

Block Image
WebFlux
Block Image
Virtual Threads

CPU/Heap:

Block Image
WebFlux
Block Image
Virtual Threads

Second load test: 200 concurrent users, repeated 20 times

  • Number of Threads (users): 200.
  • Loop Count: 20.
  • Total requests: 4000.

Average Response Time: WebFlux 374 - VT 436 (- 62 for WebFlux)

Block Image
WebFlux
Block Image
Virtual Threads

Throughput: WebFlux 453.6/sec - VT 390.9/sec (+ 62.7 for WebFlux)

Block Image
WebFlux
Block Image
Virtual Threads

CPU/Heap:

Block Image
WebFlux
Block Image
Virtual Threads

Third load test: 400 concurrent users, repeated 20 times

  • Number of Threads (users): 400.
  • Loop Count: 20.
  • Total requests: 8000.

Average Response Time: WebFlux 528 - VT 549 (- 21 for WebFlux)

Block Image
WebFlux
Block Image
Virtual Threads

Throughput: WebFlux 611.7/sec - VT 595.8/sec (+ 15.9 for WebFlux)

Block Image
WebFlux
Block Image
Virtual Threads

CPU and heap usage:

Block Image
WebFlux
Block Image
Virtual Threads

Fourth load test: 800 concurrent users, repeated 20 times

  • Number of Threads (users): 800.
  • Loop Count: 20.
  • Total requests: 16000.

Average Response Time: WebFlux 999 - VT 998 (no winner)

Block Image
WebFlux
Block Image
Virtual Threads

Throughput: WebFlux 653.2/sec - VT 614.8/sec (+ 38.4 for WebFlux)

Block Image
WebFlux
Block Image
Virtual Threads

CPU and heap usage:

Block Image
WebFlux
Block Image
Virtual Threads

Conclusions

From these tests, we see the fact that the more concurrent requests increase, the more WebFlux takes advantage over Virtual Threads, in terms of Throughput. On average response times, again WebFlux wins by a small margin, but I did not find a pattern as for Throughput.

On the CPU, it seems that WebFlux uses more CPU at low load than VT. By increasing the load of requests, the values of WebFlux and VT are comparable. Finally, with regard to Heap utilization, it appears that VT uses more memory than WebFlux.

Under these conditions, the winner of this challenge is WebFlux!
However, this article is not intended to express a preference for using WebFlux at the expense of virtual threads. The use of one or the other depends on project to project, and team to team (how confident you are in the reactive approach).

More articles on Spring: Spring.
Articles about Docker: Docker.

Recommended books on Spring, Docker, and Kubernetes: