Same load, two concurrency models โ watch where each one breaks
Reactive Streams give the consumer a way to tell the producer to slow down. Without it, a buffer between them just grows until you OOM. Toggle the mode and watch the buffer.
@RestController public class UserController { @Autowired UserRepo repo; @Autowired RestTemplate http; @GetMapping("/users/{id}") public UserDto getUser(@PathVariable Long id) { User user = repo.findById(id) // blocks thread .orElseThrow(); Profile p = http.getForObject( // blocks thread "/profile/" + id, Profile.class); return new UserDto(user, p); } }
findById + REST call duration. With 200 threads and 100ms latency you cap at ~2000 RPS before threads run out.@RestController public class UserController { @Autowired ReactiveUserRepo repo; @Autowired WebClient http; @GetMapping("/users/{id}") public Mono<UserDto> getUser(@PathVariable Long id) { return repo.findById(id) // returns Mono<User> .zipWith(http.get() // runs in parallel .uri("/profile/{id}", id) .retrieve() .bodyToMono(Profile.class)) .map(t -> new UserDto(t.getT1(), t.getT2())); } }
Thread.sleep) from here โ it stalls the entire server.flatMap is hardThread.sleep, JDBC, blocking RestTemplate all stall the server. Use .publishOn(Schedulers.boundedElastic()) if you must.Hooks.onOperatorDebug() in dev or checkpoint() at key points.contextWrite).onBackpressureBuffer() with no bound just delays the OOM.If each request waits 100ms on a database call, one thread serves at most 10 req/s. 200 threads ร 10 = 2000 RPS ceiling โ and beyond that, requests pile up in the queue and start timing out. The only knobs are: bigger pool (more memory, more context-switching) or faster downstream (not always possible).
WebFlux sidesteps this entirely: the thread is freed during I/O wait, so the same 4 threads can serve tens of thousands of concurrent requests. The new ceiling is downstream capacity or CPU, not threads.
Java 21+ virtual threads (Project Loom) let you keep MVC's simple programming model while getting WebFlux's concurrency. Each request still gets a "thread", but it's a virtual thread โ millions are cheap, and JDBC blocking calls are transparently parked instead of holding an OS thread.
For new projects on JDK 21+, this is often the better answer than WebFlux: same code as MVC, similar throughput to WebFlux, no reactive learning curve. WebFlux still wins for streaming and backpressure scenarios.