WebFlux vs. Virtual Threads: Comparison at 2000 Requests per Second
Under high loads, the choice between Spring WebFlux's reactive model and Project Loom's new virtual threads paradigm determines the performance and scalability of Java applications. This study presents load testing results for both approaches at 2000 requests per second on a sample microservices project.
Spring WebFlux Architecture: Event Loop and Non-Blocking I/O
Spring WebFlux uses a non-blocking I/O model with an event loop. Unlike the traditional "one request—one thread" model, WebFlux handles thousands of connections using a small number of threads. The key principle: a thread doesn't block while waiting for I/O; instead, it registers interest in an event (e.g., "data arrived on socket") and switches to other tasks.
The central component is Netty, which WebFlux uses by default. Netty manages the event loop via EventLoopGroup. The number of event loop threads is determined by this formula:
int DEFAULT_IO_WORKER_COUNT = Integer.parseInt(System.getProperty(
ReactorNetty.IO_WORKER_COUNT,
"" + Math.max(Runtime.getRuntime().availableProcessors(), 4)));
In other words, the number of threads equals the number of available processors, but no fewer than 4. In a containerized environment, this value can be adjusted to match allocated resources.
Each network connection (Channel) is bound to a specific event loop (channel affinity). This ensures no synchronization overhead and CPU cache efficiency. However, any blocking operation in an event loop halts processing for all connections pinned to that loop.
Virtual Threads: How Project Loom Works
Virtual threads retain the "one request—one thread" model but drastically reduce overhead costs. Each request gets its own virtual thread, which mounts on carrier threads (platform threads). Carrier threads are managed by a scheduler based on ForkJoinPool.
The default number of carrier threads equals the number of available processors. For example, in a container with 2 CPUs, there will be 2 carrier threads capable of running thousands of virtual threads.
The default scheduler creation method:
private static ForkJoinPool createDefaultScheduler() {
ForkJoinWorkerThreadFactory factory = pool -> new CarrierThread(pool);
int parallelism, maxPoolSize, minRunnable;
String parallelismValue = System.getProperty("jdk.virtualThreadScheduler.parallelism");
String maxPoolSiz
Advantages of virtual threads:
- Preserves imperative programming style
- Readable stack traces
- ThreadLocal support
- Simplifies integration with existing blocking libraries
Load Testing Results at 2000 Requests per Second
Testing on the sample microservices project revealed the following:
- WebFlux showed stable performance as long as there were no blocking operations in the event loop. However, integrating blocking libraries (e.g., JDBC) required offloading those operations to a separate thread pool (Schedulers.boundedElastic()), which added complexity.
- Virtual threads simplified development and debugging while delivering high performance. At 2000 requests per second, virtual threads on Spring MVC and Tomcat matched WebFlux throughput, with code that felt familiar to most Java developers.
A critical factor for WebFlux was properly using reactive chains and avoiding blocking calls in the event loop. For virtual threads, note that they don't address CPU-bound operations—high CPU load can lead to contention for carrier threads.
Critical Differences and Recommendations
When choosing between WebFlux and virtual threads, consider these aspects:
- Workload Type:
- Both approaches excel for I/O-bound apps (heavy I/O waiting)
- For CPU-bound tasks, virtual threads may lag due to carrier thread contention
- Library Ecosystem:
- If your project relies on blocking libraries (JDBC), virtual threads ease migration
- For new projects supporting reactive drivers (R2DBC), WebFlux is still viable
- Team Expertise:
- Reactive programming demands deep async pattern knowledge
- Virtual threads enable familiar imperative style
Key Takeaways
- Performance at 2000 requests per second: Both handle the load, but virtual threads simplify development by sticking to imperative style
- Critical Limitations: A blocking operation in WebFlux's event loop paralyzes all requests on that loop; virtual threads don't fix CPU-bound issues
- Ecosystem: Adopting WebFlux means swapping blocking libraries for reactive ones, which can be expensive
- Debugging: Reactive apps are tougher to diagnose due to messy stack traces, while virtual threads offer familiar profiling tools
— Editorial Team
No comments yet.