WebFlux in 2026: When Reactive Pays Off and When Virtual Threads Win
Foreword
From synchronous blocking to asynchronous non-blocking, another revolution in Java web development
Recently, while reviewing technology selection proposals, I noticed a very interesting phenomenon—more and more new projects are including Spring WebFlux in their tech stacks.
A colleague asked me: "Brother San, I've read articles online. Some say WebFlux has explosive performance, some say its learning curve is too steep, and others say we should go back to synchronous programming. I don't know who to believe."
That's a very good question.
WebFlux has been around for nearly 10 years since its introduction in Spring 5.0, and the discussions about it have never stopped.
Some call it "the future," others call it "a gimmick."
So what's the truth?
Today, this article will discuss WebFlux with everyone. I hope it helps you.
More project practices on my tech website: susan.net.cn/project
1. What Problem Does WebFlux Actually Solve?
Some colleagues might say at work: "I've been using Spring MVC for many years and it feels fine. Why should I learn WebFlux?"
Before answering this question, let's look at a typical Spring MVC controller:
@RestController
public class OrderController {
@GetMapping("/order/{id}")
public Order getOrder(@PathVariable Long id) {
// Calling an external API to fetch data, might take 2 seconds
User user = userService.getUser(order.getUserId()); // Thread blocks here for 2 seconds!
return order;
}
}
Where is the problem?
When a request reaches the server, the Servlet container (like Tomcat) allocates a worker thread from the thread pool to handle this request.
During the entire 2 seconds that this thread executes userService.getUser(), the thread can do nothing but spin and wait.
It cannot handle other requests that have already arrived.
If there are 1000 such concurrent requests at the same time, Tomcat needs to prepare at least 1000 threads to handle them.
Each thread consumes about 1MB of stack memory. When the number of threads exceeds the physical core capacity, a large amount of time is wasted on thread context switching, potentially leading to resource exhaustion and crashes.
This is the natural bottleneck of the "one request, one thread" blocking model in I/O-intensive scenarios.
We invest a large amount of resources (threads) just to "wait," not to "compute."
Core problem: Threads are completely idle while waiting for I/O, but resources remain occupied.
Some colleagues may have already mitigated this problem by increasing thread pools or splitting services, but this is essentially "trading resources for throughput," not an optimal solution.
When your system needs to support tens of thousands of concurrent connections, this "stacking threads" approach will eventually hit a ceiling.
The problem WebFlux aims to solve is supporting higher concurrency with fewer resources.
2. Asynchronous Non-blocking + Reactive Streams
WebFlux's philosophy is fundamentally different.
It originates from the Reactive Programming paradigm, with the core goal of: using a small, fixed number of threads to handle a large number of concurrent requests.
How is this achieved?
The answer is event-driven + asynchronous non-blocking I/O.
It no longer makes threads wait foolishly. Instead, it tells the system: "I'll go do something else. When the data is ready, notify me via callback."
2.1 Thread Model Comparison
This is the most critical diagram for understanding WebFlux:
| Comparison Dimension | Spring MVC | Spring WebFlux |
|---|---|---|
| Programming Model | Imperative / Synchronous Blocking | Declarative / Async Non-blocking + Reactive Streams |
| Thread Model | Thread-per-Request | Event-Loop + Few Threads (Netty default) |
| I/O Model | Blocking I/O | Non-blocking I/O |
| Concurrency Capability | Determined by thread pool size (usually 200-500) | Theoretically up to tens of thousands to hundreds of thousands of connections |
| Backpressure Support | None | Native support (Reactive Streams specification) |
| Default Server | Tomcat / Jetty | Netty (or Undertow) |
WebFlux's thread model is a completely different approach:
A small number of threads handle all requests through an event loop. I/O operations do not block threads; upon completion, they notify via callbacks. A single thread can handle tens of thousands of concurrent connections.
Intuitive comparison: When handling 10,000 long connections, WebFlux's CPU utilization is 40% lower than synchronous architectures, and memory consumption is reduced by 25%.
In scenarios with tens of thousands of concurrent connections, a Netty-based WebFlux application can reduce memory consumption by 30%-50% compared to traditional Servlet containers, while maintaining more stable P99 latency.
2.2 Reactor and Mono/Flux
The first hurdle to understanding WebFlux is understanding the two core types of Project Reactor:
- Mono: Represents an asynchronous sequence of 0 or 1 result. Think of it as a promise for a "single data packet that may arrive in the future."
- Flux: Represents an asynchronous sequence of 0 to N results.
// Return a single result using Mono
@GetMapping("/user/{id}")
public Mono<User> getUser(@PathVariable Long id) {
return userRepository.findById(id); // Async query, returns Mono immediately
}
// Return multiple results using Flux
@GetMapping("/users")
public Flux<User> getUsers() {
return userRepository.findAll(); // Async query, returns Flux immediately
}
Key understanding: The method returns immediately after execution, without waiting for data to be ready.
The actual data will be delivered through Mono/Flux at some point in the future.
3. Building a WebFlux Application from Scratch
Talking without practice is empty talk.
Let's spend 5 minutes building a WebFlux application.
3.1 Adding Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
spring-boot-starter-web is not needed; the two are mutually exclusive. WebFlux uses Netty as the server by default.
3.2 Reactive Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
// Return a single user
@GetMapping("/{id}")
public Mono<User> getUser(@PathVariable Long id) {
return userService.findById(id)
.switchIfEmpty(Mono.error(new UserNotFoundException(id)));
}
// Return a list of users
@GetMapping
public Flux<User> getUsers(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return userService.findAll(page, size);
}
// Create a user
@PostMapping
public Mono<User> createUser(@RequestBody Mono<User> userMono) {
return userMono.flatMap(userService::save);
}
}
Code breakdown:
- Methods return
Mono<User>orFlux<User>, returning immediately without waiting for data to be ready flatMapoperator handles asynchronous nesting, avoiding "callback hell"switchIfEmptyprovides reactive null-value handling
3.3 Reactive Service
@Service
public class UserService {
private final ReactiveUserRepository userRepository;
public UserService(ReactiveUserRepository userRepository) {
this.userRepository = userRepository;
}
public Mono<User> findById(Long id) {
return userRepository.findById(id);
}
public Flux<User> findAll(int page, int size) {
return userRepository.findAll()
.skip((long) page * size)
.take(size);
}
public Mono<User> save(User user) {
return userRepository.save(user);
}
}
3.4 Reactive Data Access (R2DBC)
WebFlux needs to be paired with R2DBC (Reactive Relational Database Connectivity) to achieve end-to-end non-blocking:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-postgresql</artifactId>
</dependency>
@Repository
public interface ReactiveUserRepository
extends ReactiveCrudRepository<User, Long> {
Mono<User> findByEmail(String email);
Flux<User> findByAgeGreaterThan(int age);
}
Key understanding: Without R2DBC, using WebFlux + traditional JDBC means database access is still blocking. End-to-end non-blocking breaks at the database layer. To make the entire chain non-blocking, database access must also be non-blocking.
4. WebClient: The Reactive HTTP Client
WebFlux provides WebClient as a reactive HTTP client, replacing the traditional RestTemplate:
@Service
public class OrderService {
private final WebClient webClient;
public OrderService(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder
.baseUrl("http://user-service")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
public Mono<User> getUserWithOrders(Long userId) {
// Parallel calls to two services
Mono<User> userMono = webClient.get()
.uri("/api/users/{id}", userId)
.retrieve()
.bodyToMono(User.class);
Flux<Order> ordersFlux = webClient.get()
.uri("/api/orders?userId={id}", userId)
.retrieve()
.bodyToFlux(Order.class);
// Merge results
return userMono.zipWith(ordersFlux.collectList())
.map(tuple -> {
User user = tuple.getT1();
user.setOrders(tuple.getT2());
return user;
});
}
}
Key point: The two external calls are executed in parallel, not serially. The zipWith operator waits for both asynchronous results to return before merging them.
If using RestTemplate, these two calls would be serial, with total time being the sum of both. With WebClient, the total time is the maximum of the two.
5. Why Are More People Using It?
5.1 Resource Efficiency Revolution in the Cloud-Native Era
In Kubernetes clusters, each Pod's resource quota is limited. WebFlux applications can support higher concurrency with less memory and CPU.
Real data: Microservice clusters adopting reactive architecture achieve 3-5x throughput improvement and over 60% latency reduction under equivalent hardware conditions. Industry surveys show that more and more enterprises are introducing WebFlux into production environments.
5.2 Streaming Data Processing and Real-time Push
WebFlux natively supports Server-Sent Events (SSE) and WebSocket, suitable for real-time data push scenarios:
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<StockPrice> streamStockPrices() {
return stockService.priceStream()
.delayElements(Duration.ofSeconds(1)); // Push every second
}
Financial quotes, IoT device data, real-time monitoring—WebFlux is a natural choice for these scenarios.
5.3 Backpressure Mechanism: Preventing System Overload
Backpressure is a core feature of the Reactive Streams specification. When the consumer's processing speed cannot keep up with the producer, the consumer can actively tell the producer to "slow down".
Flux.range(1, 1000000)
.onBackpressureBuffer(1000) // Buffer 1000 elements
.subscribe(new BaseSubscriber<Integer>() {
@Override
protected void hookOnNext(Integer value) {
// Process one
request(1); // Request the next one after processing one
}
});
The traditional Servlet model lacks a backpressure mechanism; the producer may overwhelm the consumer. WebFlux's backpressure mechanism has overwhelming advantages in streaming/big data scenarios.
5.4 GraalVM Native Image Support
In the 2025 Spring ecosystem, WebFlux's compatibility with GraalVM native images has significantly improved, with startup time optimized to within 100ms.
This is significant for Serverless and rapid elastic scaling scenarios—100ms startup time means Pods can scale out within seconds.
6. Disadvantages and Applicable Scenarios
6.1 Advantages
1. Extremely High Resource Efficiency
WebFlux handles massive concurrency with a small number of threads, meaning fewer Pods, lower memory usage, and lower cloud costs in cloud-native environments.
In scenarios with tens of thousands of concurrent connections, WebFlux can reduce memory consumption by 30%-50% while maintaining more stable P99 latency.
Netty's default Event Loop thread count usually equals the number of CPU cores, while Tomcat's thread pool can reach hundreds or even thousands—a WebFlux application's thread count under peak load may be only one-tenth of an MVC application's.
2. Streaming Data Processing and Real-time Push
WebFlux natively supports Server-Sent Events (SSE) and WebSocket. Financial quotes, IoT device data, real-time monitoring, and similar scenarios are naturally suited for it. Implementing streaming push in the traditional Servlet model requires a lot of extra handling for asynchronous support.
3. Backpressure Mechanism
When the consumer's processing speed cannot keep up with the producer, the consumer can actively tell the producer to "slow down." The traditional Servlet model lacks a backpressure mechanism; the producer may overwhelm the consumer. This has overwhelming advantages in streaming/big data scenarios.
4. End-to-End Non-blocking
From the web layer to the data access layer (paired with R2DBC), the entire chain is non-blocking. All links run on Netty's event loop, avoiding thread switching overhead.
5. WebClient Parallel Invocation Capability
WebClient supports parallel execution of multiple external service calls, using operators like zipWith and merge to combine multiple asynchronous requests. Total time changes from "sum of serial times" to "maximum of parallel times."
6. GraalVM Native Image Support
In the 2025 Spring ecosystem, WebFlux's compatibility with GraalVM native images has significantly improved, with startup time optimized to within 100ms. This is significant for Serverless and rapid elastic scaling scenarios.
7. Deep Integration with the Spring Ecosystem
WebFlux is part of the Spring ecosystem, seamlessly integrating with components like Spring Boot, Spring Security, Spring Data, and Spring Cloud without introducing additional frameworks.
6.2 Disadvantages
1. Steep Learning Curve Requires understanding reactive thinking, Mono/Flux operators, backpressure mechanisms, and other new concepts. Transitioning from imperative programming to the reactive paradigm requires a cognitive leap from synchronous to asynchronous, from state to stream, from blocking to subscription.
2. High Debugging Difficulty Stack traces in reactive code are not as intuitive as synchronous code; exceptions in flatMap chains may span multiple operators. However, Spring provides tools like BlockHound to assist in detecting thread blocking issues.
3. Ecosystem Adaptation Still Needs Time Some traditional JDBC and ORM libraries lack reactive versions, requiring alternatives like R2DBC. End-to-end non-blocking requires the entire tech stack to support reactive—if database access remains blocking, the benefits brought by WebFlux will be offset.
4. Overkill for Simple CRUD Scenarios For simple CRUD applications, the performance improvement brought by WebFlux may not offset its complexity. Moreover, with the support of virtual threads, the blocking I/O model can already more easily achieve concurrency capabilities close to WebFlux.
5. Default Configuration Has Large Tuning Space but Higher Requirements WebFlux's default configuration suits moderate loads, but in high-concurrency scenarios, Netty parameters (like maxConnections, idleTimeout, eventLoopThreads, etc.) need to be tuned according to actual business characteristics, placing higher demands on the operations team.
6.3 Applicable Scenarios
| Scenario | Recommendation Level | Reason |
|---|---|---|
| API Gateway | Strongly Recommended | High concurrency, I/O-intensive, service aggregation |
| Real-time Data Push | Strongly Recommended | Native SSE/WebSocket support |
| Inter-Microservice Calls | Strongly Recommended | WebClient parallel calls, significantly reduced latency |
| Streaming Data Processing | Strongly Recommended | Backpressure mechanism naturally suited |
| Serverless/FaaS | Recommended | Fast GraalVM native image startup |
| Traditional CRUD Applications | Needs Evaluation | Unclear benefits, high learning cost |
| CPU-Intensive Computing | Not Recommended | WebFlux's advantage is in I/O-intensive tasks; CPU-intensive tasks increase scheduling overhead instead |
Best Practice Judgment: If your application is I/O-intensive (many database queries, external API calls, file operations), WebFlux can deliver maximum value. If it's CPU-intensive, WebFlux's advantages are not obvious and may even be slightly slower than synchronous solutions due to the extra overhead of operator chains.
Best Practice Judgment: If your application is I/O-intensive (many database queries, external API calls, file operations), WebFlux can deliver maximum value. If it's CPU-intensive, WebFlux's advantages are not obvious.
7. Virtual Threads Have Arrived
In 2026, an important new variable emerged in the Java ecosystem—Virtual Threads (Project Loom).
Virtual threads allow synchronous blocking code to support high concurrency at extremely low cost.
With Tomcat + virtual threads, developers can use the familiar synchronous programming model to achieve concurrency capabilities close to WebFlux.
So will WebFlux be replaced?
Currently, the two are complementary.
Virtual threads suit most traditional business scenarios, allowing synchronous code to support high concurrency.
WebFlux still has irreplaceable advantages in scenarios like streaming data processing, backpressure control, and fine-grained thread scheduling.
Technology selection in 2026 is no longer "either/or" but choosing the most suitable solution based on specific scenarios.
More project practices on my tech website: susan.net.cn/project
8. Final Words
Returning to the original question: Why are more and more people using WebFlux?
First, a revolution in resource efficiency. WebFlux handles massive concurrency with a small number of threads, meaning fewer Pods, lower memory usage, and lower cloud costs in cloud-native environments. In scenarios with tens of thousands of concurrent connections, WebFlux can reduce memory consumption by 30%-50%.
Second, natural adaptation for streaming data processing. SSE, WebSocket, backpressure mechanisms—these capabilities are either unsupported or very awkward to implement in the traditional Servlet model. WebFlux makes streaming data processing elegant.
Third, an inevitable choice in the cloud-native era. In Serverless and rapid elastic scaling scenarios, WebFlux + GraalVM native image startup time can be optimized to within 100ms. This startup speed is hard for traditional JVM applications to match.
Of course, WebFlux is not a silver bullet.
The steep learning curve, high debugging difficulty, and still-maturing ecosystem—these shortcomings objectively exist.
Moreover, the emergence of virtual threads in 2026 has given synchronous programming high concurrency capabilities as well.
My suggestion is: If your application is I/O-intensive, needs to support high concurrency, or requires streaming data processing capabilities, WebFlux is worth your time to learn deeply.
If it's just a simple CRUD application, traditional Spring MVC + virtual threads may be a more pragmatic choice.
There is no standard answer in technology selection.
Understanding your own business scenarios and choosing the most suitable technical solution is what an architect truly needs to do.
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
After Java 21 virtual threads, whether WebFlux or Vert.x, the number of users has probably dropped, right?