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.
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.propertiesconfiguration:
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 Userentity (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.propertiesconfiguration:
server.port=3000
spring.r2dbc.url=r2dbc:mysql://localhost:3306/testdb
spring.r2dbc.username=dbser
spring.r2dbc.password=dbpwd Userentity (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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
