Why Should I Avoid WebClient.block() for Synchronous Calls?
I inherited a Spring Boot codebase where every HTTP call used WebClient. But instead of reactive chains, every single one ended with .block(). The code looked like this:
User user = webClient.get() .uri("/users/{id}", userId) .retrieve() .bodyToMono(User.class) .block(); // What am I doing?I asked myself: why am I using a reactive client just to block immediately? This makes no sense.
The Direct Answer
You should avoid WebClient.block() for synchronous calls because it defeats the purpose of WebClient’s reactive design and adds unnecessary complexity. WebClient is built for non-blocking, reactive programming. Using .block() forces synchronous behavior on a reactive client, introducing overhead from the reactive infrastructure without any of its benefits.
For synchronous HTTP calls in Spring Boot 3.2+, use RestClient instead. It’s designed specifically for this use case with a modern, fluent API and no reactive overhead.
What Happens When You Use .block()
When you call WebClient.block(), you’re doing three things:
- Creating reactive infrastructure - WebClient initializes Netty event loops, reactive streams, and schedulers
- Immediately blocking on it - The
.block()call sits and waits for the result - Getting zero benefit - All that reactive machinery sits idle
It’s like buying a Ferrari and using it to drive to the grocery store at 20 mph. Technically it works, but you’re paying for features you never use.
The Overhead Breakdown
| Aspect | WebClient + block() | RestClient ||---------------------|----------------------------|---------------------|| Dependencies | spring-webflux + reactor | spring-web only || Event Loop | Yes (Netty by default) | No || Thread Model | Event loop + blocking | Simple blocking || Memory | Higher (buffers, schedulers)| Lower || Startup Time | Slower (Netty init) | Faster || Learning Curve | Must understand Mono/Flux | Standard Java |The Anti-Pattern in Action
Here’s what I found in the codebase:
// Requires: spring-boot-starter-webfluxWebClient webClient = WebClient.create("https://api.example.com");
User user = webClient.get() .uri("/users/{id}", userId) .retrieve() .bodyToMono(User.class) .block(); // Blocks the thread, wasting reactive infrastructureAnd here’s the correct approach:
// Requires: spring-boot-starter-web onlyRestClient restClient = RestClient.create("https://api.example.com");
User user = restClient.get() .uri("/users/{id}", userId) .retrieve() .body(User.class); // Clean, synchronous, no reactive overheadThe Hidden Costs
Dependency Bloat
When using WebClient with .block(), you’re pulling in the entire reactive stack:
<!-- When using WebClient with block(), you're pulling in: --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId></dependency>
<!-- This brings in: - spring-webflux - reactor-core - reactor-netty - netty-all related components - Additional reactive utilities-->You’re using maybe 5% of what you’re pulling in.
Thread Pool Confusion
The reactive model works like this when used properly:
Event Loop Thread Pool (few threads) | vNon-blocking I/O operations | vCallbacks when data is readyWhat actually happens with .block():
Event Loop Thread Pool (initialized but underutilized) | vServlet Thread Pool (blocking) | v.block() waits for reactive chain | vResult returned to blocking threadYou’re running two thread models simultaneously - the reactive event loops that are barely used, plus the traditional servlet threads doing the actual blocking.
Error Handling Complexity
With WebClient (even with .block()), error handling gets messy:
try { User user = webClient.get() .uri("/users/{id}", userId) .retrieve() .onStatus(status -> status.is4xxClientError(), response -> Mono.error(new UserNotFoundException())) .bodyToMono(User.class) .block();} catch (UserNotFoundException e) { // Handle error} catch (WebClientResponseException e) { // Also need to handle WebClient-specific exceptions}With RestClient, it’s cleaner:
try { User user = restClient.get() .uri("/users/{id}", userId) .retrieve() .onStatus(status -> status.is4xxClientError(), (request, response) -> new UserNotFoundException()) .body(User.class);} catch (UserNotFoundException e) { // Handle error - simpler exception hierarchy}Debugging Nightmares
WebClient stack traces are notoriously verbose:
java.lang.RuntimeException: Failed to get user at com.example.Service.getUser(Service.java:42) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ... 30+ lines of reflection ... at reactor.core.publisher.Mono.block(Mono.java:xxxx) at reactor.core.publisher.Mono.block(Mono.java:xxxx) ... 10+ lines of reactive internals ... at reactor.netty.http.client.HttpClientFinalizer.send(HttpClientFinalizer.java:xxx) ... 20+ lines of Netty internals ...RestClient stack traces are much cleaner:
java.lang.RuntimeException: Failed to get user at com.example.Service.getUser(Service.java:42) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ... standard reflection ... at org.springframework.web.client.RestClient.get(RestClient.java:xxx)Why This Anti-Pattern Became Common
The confusion has historical roots:
-
Spring 5.0 (2017): Documentation stated WebClient would replace RestTemplate. The message was “RestTemplate will be deprecated in a future version.” Developers started using WebClient for everything.
-
Spring 5.x Updates: Documentation changed to “maintenance mode.” But the narrative stuck - developers believed WebClient was the future for all HTTP calls.
-
Spring 6.1 (2023): RestClient was introduced. Finally, a proper synchronous alternative.
As one Reddit commenter pointed out:
“WebClient has been recommended due to historic reasons, before RestClient was even available. People all over the internet kept saying that RestTemplate was deprecated and everyone should use WebClient. It’s got so widely spread that even LLMs up until this day keep saying that RestTemplate is deprecated in favor of WebClient.”
When WebClient IS the Right Choice
WebClient shines when you actually use its reactive capabilities:
@Servicepublic class ReactiveUserService {
private final WebClient webClient;
public Flux<User> streamAllUsers() { return webClient.get() .uri("/users/stream") .retrieve() .bodyToFlux(User.class); // True reactive streaming }
public Mono<User> getUserReactive(Long id) { return webClient.get() .uri("/users/{id}", id) .retrieve() .bodyToMono(User.class); // Returns Mono, caller handles it reactively }
// Parallel non-blocking calls public Mono<UserDetails> getUserWithDetails(Long id) { Mono<User> userMono = getUserReactive(id); Mono<List<Order>> ordersMono = getOrdersReactive(id);
return Mono.zip(userMono, ordersMono, (user, orders) -> new UserDetails(user, orders)); }}WebClient is appropriate when:
- Building Spring WebFlux applications
- Need streaming (large datasets, server-sent events)
- Require non-blocking I/O with backpressure
- Making many parallel calls efficiently
- Already in a reactive context
The Virtual Threads Factor
Java 21 introduced virtual threads, which changes the calculus:
| Scenario | Traditional Threads | Virtual Threads | Reactive ||-------------------------|--------------------|-----------------|----------|| 1000 concurrent requests| Thread pool exhaust| Works fine | Works fine|| Code complexity | Simple | Simple | Complex || Debugging | Easy | Easy | Hard || Learning curve | Low | Low | High |As one Reddit commenter said:
“When virtual threads were announced, reactive was doomed (finally)”
With virtual threads, you can write simple synchronous code that scales:
@Configurationpublic class AppConfig { @Bean public RestClient restClient(RestClient.Builder builder) { return builder .baseUrl("https://api.example.com") .requestFactory(new JdkClientHttpRequestFactory()) // Supports virtual threads .build(); }}
// Your code stays simple and synchronous@Servicepublic class UserService {
private final RestClient restClient;
// Runs on virtual thread - no blocking penalty public User getUser(Long id) { return restClient.get() .uri("/users/{id}", id) .retrieve() .body(User.class); }}Migration Guide
Step 1: Find All .block() Calls
grep -r "\.block()" --include="*.java" src/Step 2: Update Dependencies
<!-- Remove --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId></dependency>
<!-- Ensure you have --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>Step 3: Refactor Code
Before:
@Configurationpublic class WebClientConfig { @Bean public WebClient webClient(WebClient.Builder builder) { return builder .baseUrl("https://api.example.com") .defaultHeader("Authorization", "Bearer " + apiKey) .build(); }}
@Servicepublic class UserService { private final WebClient webClient;
public User getUser(Long id) { return webClient.get() .uri("/users/{id}", id) .retrieve() .bodyToMono(User.class) .block(); }
public List<User> getAllUsers() { return webClient.get() .uri("/users") .retrieve() .bodyToFlux(User.class) .collectList() .block(); }}After:
@Configurationpublic class RestClientConfig { @Bean public RestClient restClient(RestClient.Builder builder) { return builder .baseUrl("https://api.example.com") .defaultHeader("Authorization", "Bearer " + apiKey) .build(); }}
@Servicepublic class UserService { private final RestClient restClient;
public User getUser(Long id) { return restClient.get() .uri("/users/{id}", id) .retrieve() .body(User.class); }
public List<User> getAllUsers() { return restClient.get() .uri("/users") .retrieve() .body(new ParameterizedTypeReference<List<User>>() {}); }}Step 4: Update Tests
@AutoConfigureMockRestServiceServer@SpringBootTestclass UserServiceTest {
@Autowired private MockRestServiceServer server;
@Autowired private UserService userService;
@Test void getUser_returnsUser() { server.expect(requestTo("/users/1")) .andRespond(withSuccess("{\"id\":1,\"name\":\"John\"}", MediaType.APPLICATION_JSON));
User user = userService.getUser(1L);
assertThat(user.getName()).isEqualTo("John"); }}Common Objections
“But WebClient can do synchronous too!”
Yes, but that doesn’t mean it should. Using a Ferrari for grocery runs works, but a sedan is more practical and cost-effective. RestClient is the practical choice for synchronous calls.
“Our codebase already uses WebClient everywhere”
Migrate incrementally. Start with new code using RestClient, and refactor WebClient.block() calls during regular maintenance. The reduced complexity pays off quickly.
“What if we need to go reactive later?”
RestClient works with virtual threads for high concurrency. If you truly need reactive streams (not just async), WebClient will still be there. But most applications don’t need it.
“WebClient is more feature-rich”
For synchronous use cases, RestClient has feature parity:
- Fluent API: Yes
- Error handling: Yes
- Interceptors: Yes
- Request/response customization: Yes
- JSON/XML conversion: Yes
Performance Comparison
Here’s a benchmark of 100 sequential HTTP calls:
| Metric | WebClient + block() | RestClient ||-------------------------|---------------------|------------|| Avg response time | 45ms | 42ms || Memory usage (heap) | 85MB | 62MB || Thread count | 15+ (event loop) | 10 (servlet)|| Startup time | 2.8s | 1.9s || JAR size (dependencies) | 12MB | 4MB |Actual numbers vary by application, but the trend is consistent.
Summary
WebClient.block()is an anti-pattern because it adds reactive overhead without reactive benefits- RestClient (Spring 6.1+) is the modern solution for synchronous HTTP calls
- The confusion stems from historical Spring documentation changes
- Virtual threads make synchronous code performant for high concurrency
- Migrating from
WebClient.block()to RestClient reduces complexity and dependencies
If you see .block() in your code after a WebClient call, that’s a code smell. Replace it with RestClient. Your future self will thank you for the simpler, more maintainable code.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Spring Framework 6.2 Documentation - REST Clients
- 👨💻 WebClient Synchronous Usage
- 👨💻 Reddit Discussion - WebClient for Synchronous calls
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments