Spring Boot Virtual Threads vs WebFlux: Which Delivers Better Performance?

This article summarizes a performance comparison between Spring Boot applications using virtual threads and those built with Spring WebFlux, detailing the test scenario of JWT verification and MySQL queries, the environment, code implementations, benchmark results across various concurrency levels, and concluding that WebFlux outperforms virtual‑threaded Spring Boot.

Programmer DD
Programmer DD
Programmer DD
Spring Boot Virtual Threads vs WebFlux: Which Delivers Better Performance?

In the morning I saw an article comparing Spring Boot virtual threads and WebFlux performance, and I summarize the core content for quick reading.

Test Scenario

The author used a realistic scenario:

Extract JWT from the Authorization header.

Validate the JWT and extract the user's email.

Query MySQL with the email.

Return the user record.

Test Technologies

The two core technologies compared are:

Spring Boot with virtual threads: a Spring Boot application running on lightweight virtual threads that simplify development, maintenance, and debugging of high‑throughput concurrent applications. When a virtual thread encounters blocking I/O, the Java runtime suspends it and releases the underlying OS thread for other virtual threads, improving resource allocation and responsiveness.

Spring Boot WebFlux: the reactive programming framework in the Spring ecosystem that uses Project Reactor for non‑blocking, event‑driven programming, making it suitable for high concurrency and low latency workloads while providing flexibility for various data sources and protocols.

Both aim to provide high concurrency; which one performs better?

Test Environment

Runtime and Tools

MacBook Pro M1 with 16 GB RAM

Java 20

Spring Boot 3.1.3

Preview mode enabled for virtual threads

Dependencies: jjwt, mysql‑connector‑java

Benchmark tool: Bombardier

Database: MySQL

Data Preparation

Prepare a list of 100 000 JWTs in Bombardier for random selection.

Create a users table in MySQL with columns: email (PK), first, last, city, county, age.

Insert 100 000 user rows.

Test Code

Spring Boot with Virtual Threads

application.properties

configuration:

server.port=3000

spring.datasource.url= jdbc:mysql://localhost:3306/testdb?useSSL=false
spring.datasource.username= testuser
spring.datasource.password= testpwd
spring.jpa.hibernate.ddl-auto= update
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
User

entity (getters/setters omitted):

@Entity
@Table(name = "users")
public class User {
  @Id
  private String email;
  private String first;
  private String last;
  private String city;
  private String county;
  private int age;
}

Application main class:

@SpringBootApplication
public class UserApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }

    @Bean
    public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
        return protocolHandler -> {
            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
        };
    }
}

CRUD repository:

import org.springframework.data.repository.CrudRepository;
import com.example.demo.User;

public interface UserRepository extends CrudRepository<User, String> {
}

Controller:

@RestController
public class UserController {

    @Autowired
    UserRepository userRepository;

    private SignatureAlgorithm sa = SignatureAlgorithm.HS256;
    private String jwtSecret = System.getenv("JWT_SECRET");

    @GetMapping("/")
    public User handleRequest(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHdr) {
        String jwtString = authHdr.replace("Bearer", "");
        Claims claims = Jwts.parser()
            .setSigningKey(jwtSecret.getBytes())
            .parseClaimsJws(jwtString).getBody();

        Optional<User> user = userRepository.findById((String)claims.get("email"));
        return user.get();
    }
}

Spring Boot WebFlux

application.properties

configuration:

server.port=3000

spring.r2dbc.url=r2dbc:mysql://localhost:3306/testdb
spring.r2dbc.username=dbser
spring.r2dbc.password=dbpwd
User

entity (constructors, getters, setters omitted):

public class User {

  @Id
  private String email;
  private String first;
  private String last;
  private String city;
  private String county;
  private int age;
  // constructors, getters, setters omitted
}

Application main class:

@EnableWebFlux
@SpringBootApplication
public class UserApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }

}

Reactive repository:

public interface UserRepository extends R2dbcRepository<User, String> {
}

Service:

@Service
public class UserService {

    @Autowired
    UserRepository userRepository;

    public Mono<User> findById(String id) {
        return userRepository.findById(id);
    }
}

Controller:

@RestController
@RequestMapping("/")
public class UserController {

    @Autowired
    UserService userService;

    private SignatureAlgorithm sa = SignatureAlgorithm.HS256;
    private String jwtSecret = System.getenv("JWT_SECRET");

    @GetMapping("/")
    @ResponseStatus(HttpStatus.OK)
    public Mono<User> getUserById(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHdr) {
        String jwtString = authHdr.replace("Bearer", "");
        Claims claims = Jwts.parser()
            .setSigningKey(jwtSecret.getBytes())
            .parseClaimsJws(jwtString).getBody();
        return userService.findById((String)claims.get("email"));
    }
}

Test Results

The author performed 5 million requests for each solution, testing concurrency levels of 50, 100 and 300 connections.

Results (images):

50 concurrent connections

100 concurrent connections

300 concurrent connections

The conclusion is that Spring Boot WebFlux performs better than Spring Boot with virtual threads.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

performance benchmarkSpring BootmysqlWebFluxVirtual ThreadsJWT
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

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.