Why Virtual Threads Lag Behind WebFlux in Spring Boot MySQL Performance

This article examines a performance comparison of Spring Boot applications using physical threads, Java virtual threads, and WebFlux for JWT verification with MySQL, detailing test setup, code examples, benchmark results across various concurrency levels, and concluding that virtual threads perform worst due to MySQL driver incompatibility.

Programmer DD
Programmer DD
Programmer DD
Why Virtual Threads Lag Behind WebFlux in Spring Boot MySQL Performance

Evaluation Cases

Previously shared articles covered Java 21 virtual thread performance tests. This article revisits related evaluations, focusing on MySQL driver behavior under physical and virtual threads.

Test Environment

Java 20 (preview mode, virtual threads enabled)

Spring Boot 3.1.3

Dependencies: jjwt, mysql-connector-java

Test tool: Bombardier

Bombardier is used to generate load; 100,000 JWTs are pre‑created and randomly selected for each request.

MySQL table structure and data preparation

mysql> desc users;
+--------+--------------+------+-----+---------+-------+
| Field  | Type         | Null | Key | Default | Extra |
+--------+--------------+------+-----+---------+-------+
| email  | varchar(255) | NO   | PRI | NULL    |       |
| first  | varchar(255) | YES  |     | NULL    |       |
| last   | varchar(255) | YES  |     | NULL    |       |
| city   | varchar(255) | YES  |     | NULL    |       |
| county | varchar(255) | YES  |     | NULL    |       |
| age    | int          | YES  |     | NULL    |       |
+--------+--------------+------+-----+---------+-------+
6 rows in set (0.00 sec)
mysql> select count(*) from users;
+----------+
| count(*) |
+----------+
|   99999 |
+----------+
1 row in set (0.01 sec)

Test code: Physical threads

Configuration file:

server.port=3000
spring.datasource.url=jdbc:mysql://localhost:3306/testdb?useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=dbuser
spring.datasource.password=dbpwd
spring.jpa.hibernate.ddl-auto=update
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect

User entity definition:

@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;
    // getters and setters omitted
}

Data access implementation:

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

API implementation:

@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();
    }
}

Application main class:

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

Test code: Virtual threads

The main class is modified to use a virtual‑thread executor:

@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());
        };
    }
}

Test code: WebFlux

R2DBC configuration:

server.port=3000
spring.r2dbc.url=r2dbc:mysql://localhost:3306/testdb?allowPublicKeyRetrieval=true&ssl=false
spring.r2dbc.username=dbuser
spring.r2dbc.password=dbpwd
spring.r2dbc.pool.initial-size=10
spring.r2dbc.pool.max-size=10

User entity for R2DBC:

@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;
    // getters, setters, constructors omitted
}

Data access:

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

Service layer:

@Service
public class UserService {
    @Autowired
    UserRepository userRepository;

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

Controller implementation:

@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"));
    }
}

Main class for WebFlux:

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

Test results

Each benchmark sent 1,000,000 requests and measured latency/throughput at concurrency levels 50, 100, and 300. The following images show the performance curves for the three implementations.

Analysis and Summary

The benchmark reveals that the MySQL driver performs poorly with virtual threads, while WebFlux consistently delivers the best throughput and lowest latency. The root cause is the MySQL driver’s lack of virtual‑thread friendliness. When database access is required, select a driver that fully supports virtual threads. The tests were conducted with Java 20 and Spring Boot 3.1; users of Java 21 and Spring Boot 3.2 should perform their own evaluations.

Readers are encouraged to share MySQL drivers that work well 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.

Spring BootmysqlWebFluxVirtual Threads
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.