Spring WebFlux is the reactive, non-blocking sibling of Spring MVC. Instead of one thread per request blocking on I/O, WebFlux runs on a small event-loop and frees the thread while it waits for the database or another service to answer. Built on Project Reactor, it’s a great fit for high-concurrency APIs and reactive data sources — but it’s not a free upgrade, and that nuance is the most important thing to get right.
MVC vs WebFlux in one paragraph
Spring MVC uses the classic thread-per-request model: each request holds a thread until it’s done, blocking on I/O along the way. That’s simple and fine for most apps. WebFlux uses a non-blocking event loop: a handful of threads juggle thousands of in-flight requests, never blocking — when work is waiting on I/O, the thread goes off and serves someone else. More concurrency, fewer threads, less memory under load. The cost is a different, harder programming model.
Returning Mono and Flux
A WebFlux controller returns reactive types instead of plain objects:
@RestController
public class UserController {
private final UserRepository users; // reactive repository
public UserController(UserRepository users) {
this.users = users;
}
@GetMapping("/users/{id}")
public Mono<User> byId(@PathVariable String id) {
return users.findById(id); // 0 or 1 element
}
@GetMapping("/users")
public Flux<User> all() {
return users.findAll(); // 0..N elements, streamed
}
}
Mono<T>= a stream of zero or one item (a single user, a save result).Flux<T>= a stream of zero to many items (a list, a feed, an SSE stream).
You return the pipeline, not the value. The framework subscribes, handles backpressure (so a slow client can’t overwhelm a fast producer), and writes the response as data arrives. The golden rule: never call a blocking method inside the chain — that stalls the event loop and defeats the entire model.
Annotated vs functional routing
WebFlux gives you two equivalent styles on the same reactive engine:
Annotated controllers — look just like Spring MVC (the example above). Familiar, easy to adopt.
Functional endpoints — define routes as data with a router function:
@Bean
RouterFunction<ServerResponse> routes(UserHandler handler) {
return route(GET("/users/{id}"), handler::byId)
.andRoute(GET("/users"), handler::all);
}
Both integrate with the same stack — validation, exception handling, and security all work with reactive types — so the choice is team preference. Annotated is the gentler on-ramp; functional keeps routing explicit and centralized.
When (and when not) to use it
This is the part most tutorials skip. WebFlux is only worth it if your whole I/O path is non-blocking. One blocking call poisons the benefit:
Reach for WebFlux when:
- You have very high concurrency with lots of I/O wait (many slow downstream calls).
- Your data layer is reactive — R2DBC, reactive Mongo, reactive Redis.
- You’re streaming (Server-Sent Events, large datasets, chat).
Stick with Spring MVC when:
- Your database driver is the classic blocking JDBC (most apps). A blocking driver behind a reactive API gives you the complexity of reactive with none of the throughput.
- Your team isn’t fluent in reactive debugging (stack traces are harder, and a stray
.block()can deadlock). - Concurrency is moderate — MVC on modern hardware (and Java 21 virtual threads) handles a lot.
[needs source]
Virtual threads, in particular, now give blocking-style MVC code much of the scalability that used to require reactive — so “I need to handle more connections” is no longer an automatic vote for WebFlux.
Common gotchas
- A blocking call in the chain. JDBC,
RestTemplate,Thread.sleep, file I/O — any of them stall the event loop. Use reactive clients (WebClient, R2DBC) end to end. - Calling
.block(). It “works” in tests and then deadlocks in production. Treat it as a code smell outside of narrow, well-understood cases. - Expecting reactive to be faster per request. It isn’t — latency per request is similar or slightly worse. The win is throughput and resource usage under concurrency, not raw speed.
- Mixing blocking JPA with WebFlux. The classic mistake. If you’re on JPA/JDBC, you probably want MVC.
FAQ
What's the difference between Mono and Flux?
Mono<T> emits zero or one item; Flux<T> emits zero to many. Use Mono for a single result (one entity, a save), Flux for collections or streams.
Is WebFlux faster than Spring MVC?
Can I use WebFlux with a regular SQL database?
Do virtual threads make WebFlux obsolete?
Key takeaway: Spring WebFlux returns Mono/Flux on a non-blocking event loop for high-concurrency, I/O-heavy, or streaming workloads. It’s only a win if your entire stack is non-blocking — with classic JDBC/JPA, stay on Spring MVC.
Building plain REST first? Start with a Spring Boot REST controller.