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?
Not per request — latency is comparable. WebFlux wins on throughput and memory under high concurrency with non-blocking I/O. For ordinary CRUD on a blocking database, MVC is simpler and just as fast.
Can I use WebFlux with a regular SQL database?
Only meaningfully via a reactive driver like R2DBC. Classic JDBC/JPA is blocking, which cancels out WebFlux’s benefits — for that stack, use Spring MVC.
Do virtual threads make WebFlux obsolete?
No, but they narrow the gap. Java 21 virtual threads let blocking-style MVC scale to many concurrent connections, so reactive is now mostly for streaming and genuinely reactive stacks rather than raw connection counts.

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.