Cookies

This website uses cookies to obtain statistics from users navigation. If you go on browsing we consider you accept them.

Java 21 service with virtual threads performance test

I’m back to share the results of a comparison I was curious about: comparing the scalability of a simple microservice developed with Spring Boot. The idea behind this exercise is to test how Virtual Threads (one of the most famous Java 21 new features) impact on service availability.

The service will be responsible for providing a simple REST API to store, read, modify, and delete locations in a PostgreSQL database. The code is available in the GitHub repository spring-boot-location-api.

In order to develop this microservice I’ve used the traditional technological stack of Spring Boot, with the Spring Web and Spring Data JDBC starters. To the most curious, in the repository branch webflux-r2dbc I’ve also added an equivalent reactive implementation using Spring Webflux and Spring Data R2DBC.

How to execute the load tests

To measure the availability with and without virtual threads, I will use the command-line program siege, which provides a straightforward interface for executing load tests. Let’s take a look at an example:

siege -b -c 1200 -t 60s http://127.0.0.1:8080/location/1

This command will launch 1,200 concurrent HTTP requests (using the -c option) against the specified endpoint for 60 seconds (using the -t option). With this command, the availability of the endpoint that responds to GET requests can be measured with the different microservice configurations.

Virtual threads and blocking services

Let’s check the benefits of using Virtual Threads in a traditional blocking application. Traditional blocking applications typically use a thread to handle each incoming request. By default, the JVM uses an operating system thread to handle these HTTP requests. By enabling Virtual Threads, the Java machine will scale better in scenarios where multiple threads are blocked by I/O operations, as Virtual Threads are specifically optimized for such workloads and do not require the same resources as an OS thread.

In Spring Boot applications, we can activate Virtual Threads simply by adding the property:

spring.threads.virtual.enabled=true

Comparing the HTTP GET method

Let’s see what the results of our location API are with and without Virtual Threads by sending a request that will execute a query against the database.

Test with Virtual Threads disabled

{
    "transactions":                        53754,
    "availability":                        98.75,
    "data_transferred":                     8.61,
    "response_time":                        1.10,
    "transaction_rate":                   901.61,
    "throughput":                           0.14,
    "concurrency":                        989.58,
    "successful_transactions":             53764,
    "failed_transactions":                   681,
    "longest_transaction":                 31.17,
    "shortest_transaction":                 0.00
}

Test with Virtual Threads enabled

{
    "transactions":                        60490,
    "availability":                        99.24,
    "data_transferred":                     9.69,
    "response_time":                        0.99,
    "transaction_rate":                  1011.20,
    "concurrency":                        998.96,
    "successful_transactions":             60509,
    "failed_transactions":                   465,
    "longest_transaction":                 12.33,
    "shortest_transaction":                 0.00 
}                                                    

The availability provided by the solution with virtual threads is superior. Additionally, it is capable of handling 13% more requests within the same time interval without any changes to our code, resulting in significantly improved scalability.

Comparing the HTTP POST method

Now let’s examine the performance of the location persistence endpoint. This test has consistently been conducted with an empty table to ensure equal conditions between executions with and without virtual threads.

Test with Virtual Threads disnabled

{   
    "transactions":                        45858,
    "availability":                        98.29,
    "data_transferred":                     5.51,
    "response_time":                        1.28,
    "transaction_rate":                   772.28,
    "concurrency":                        988.10,
    "successful_transactions":             45885,
    "failed_transactions":                   800,
    "longest_transaction":                 11.32,
    "shortest_transaction":                 0.00
}

Test with Virtual Threads enabled

{
    "transactions":                        53267,
    "availability":                        99.28,
    "data_transferred":                     6.40,
    "response_time":                        1.11,
    "transaction_rate":                   894.79,
    "concurrency":                        996.25,
    "successful_transactions":             53277,
    "failed_transactions":                   385,
    "longest_transaction":                 35.83,
    "shortest_transaction":                 0.00
}

In this case the service availability provided by the virtual threads solution is also better, being able to respond to 16% more requests.

Summing up

Definitely, virtual threads offer performance advantages for our blocking Java services. If your projects are already using Java 21 or migrating those that receive heavier loads to this version do not require a significant effort, it could be a good idea to consider starting to use virtual threads in your Java services as soon as possible.