Spring Boot WebClient: Reactive HTTP Client Guide for Production Systems
Learn Spring Boot WebClient for reactive HTTP calls.
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
- ✓Java 17+ installed
- ✓Spring Boot 3.x project (Maven or Gradle)
- ✓Basic knowledge of Spring Boot and REST APIs
• WebClient is Spring WebFlux's reactive HTTP client replacing RestTemplate in Spring Boot 2.0+ and later. • It supports both synchronous (block) and asynchronous (non-blocking) calls. • Key features: reactive streams, retry with backoff, timeouts, custom error handling, and exchange filter chains. • Use it for high-throughput microservices communication, especially with other reactive services. • Avoid blocking calls in reactive pipelines; use block() only at the edge of your application.
Think of WebClient as a super-efficient delivery person who can carry multiple packages at once and doesn't wait for each delivery to complete before starting the next. RestTemplate is like a delivery person who delivers one package, waits for confirmation, then goes to the next. WebClient can handle many requests concurrently with fewer resources because it doesn't block threads waiting for responses.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
If you've been living under a rock and still using RestTemplate in 2024, it's time to wake up. Spring Boot 2.0 officially deprecated RestTemplate in favor of WebClient, and Spring Boot 3.0 removed it from the WebFlux stack entirely. Don't panic—RestTemplate is still available in Spring MVC projects, but you're missing out on better performance and cleaner code.
WebClient is a reactive, non-blocking HTTP client that's part of Spring WebFlux. It supports both synchronous and asynchronous communication, making it ideal for microservices that need to make many concurrent HTTP calls without exhausting thread pools. Under the hood, it uses Reactor Project's Mono and Flux types, giving you full access to reactive streams operators for composition, error handling, and backpressure.
In this guide, I'll walk you through setting up WebClient, making GET and POST requests, handling errors gracefully, configuring timeouts and retries, and integrating it into a Spring Boot application. We'll also cover production patterns like circuit breakers and observability. By the end, you'll be able to replace your old RestTemplate code with WebClient and sleep better knowing your app can handle load spikes without thread starvation.
Setting Up WebClient in Spring Boot 3.x
First, add the Spring WebFlux dependency to your project. Even if you're not building a full reactive web application, WebClient works perfectly with Spring MVC. Add this to your pom.xml:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency>
For Gradle: implementation 'org.springframework.boot:spring-boot-starter-webflux'
Now create a WebClient bean. I prefer using a configuration class that defines a custom WebClient with sensible defaults: connection timeouts, read timeouts, and a base URL. Avoid using the default WebClient.create() because it has no timeouts and can hang indefinitely.
Here's a production-ready configuration:
HttpClient.create().connectionProvider(ConnectionProvider.builder("myPool").maxConnections(500).maxIdleTime(Duration.ofSeconds(30)).build()).What the Official Docs Won't Tell You
The official Spring documentation shows you how to make a simple GET request with WebClient. It's clean and looks easy. But in production, you'll face issues they don't mention:
- Blocking in reactive pipelines: You can't call .block() inside a reactive stream (e.g., inside a flatMap). It will throw an exception in Reactor's non-blocking threads. Always use .flatMap() or .then() to compose.
- Error propagation: WebClient throws WebClientResponseException for 4xx/5xx responses. But if you don't handle it, the error will propagate as an error signal in the reactive stream, causing the whole pipeline to fail. Use .onStatus() to map specific HTTP statuses to custom exceptions.
- Retries are not built-in: WebClient doesn't retry automatically. You must use Reactor's .retryWhen() operator with an exponential backoff strategy. Otherwise, transient network failures will crash your calls.
- Memory leaks with large responses: If you read a large response body into memory without streaming, you can cause OOM. Use .bodyToFlux() for streaming large datasets.
- Threading model: By default, WebClient uses Netty's event loop threads. Don't block them with long computations. Offload CPU-heavy work to a separate scheduler using .publishOn(
Schedulers.boundedElastic()).
Making GET and POST Requests with WebClient
Let's get practical. We'll make a GET request to fetch a user and a POST request to create a payment. The key difference from RestTemplate is that WebClient returns Mono or Flux, which you can subscribe to asynchronously or block at the edge.
For a GET request, use .retrieve() to get the response body directly. For a POST, use .bodyValue() to send a JSON payload. Always specify the response type with .bodyToMono() for a single object or .bodyToFlux() for a stream.
Here's a service class that demonstrates both:
Advanced Error Handling and Retry Strategies
HTTP calls fail. Networks drop packets. Services go down. Your code must handle these gracefully. WebClient combined with Reactor gives you powerful error handling and retry mechanisms.
Use .onStatus() to map HTTP errors to custom exceptions. For transient failures (5xx, network timeouts), use .retryWhen() with exponential backoff. Avoid retrying on 4xx errors because they're client-side issues and retrying won't help.
Here's a robust error handling pattern:
Timeouts and Connection Pooling Configuration
Default WebClient configuration is dangerous for production. Without explicit timeouts, a slow or hanging service can block your application indefinitely. You must configure connection timeouts, read timeouts, and write timeouts at the HttpClient level.
Connection pooling is equally important. By default, Netty uses a connection pool with unlimited connections, which can lead to resource exhaustion. Configure a pool with max connections, max idle time, and connection acquire timeout.
Here's a production-grade HttpClient configuration:
Exchange Filter Functions for Cross-Cutting Concerns
ExchangeFilterFunction allows you to add custom logic to every request and response. This is perfect for adding authentication headers, logging, correlation IDs, or even retrying failed requests automatically.
You can chain multiple filters. They execute in order. A common pattern is to add a logging filter that measures request duration, then an authentication filter that adds a JWT token.
Here's how to create and use a logging filter:
Testing WebClient with MockWebServer
Unit testing WebClient calls requires mocking the HTTP server, not just the client. Use OkHttp's MockWebServer (part of the okhttp3 library) to simulate HTTP responses. This is more reliable than mocking WebClient itself because it tests the actual HTTP client configuration.
Add the dependency: <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>mockwebserver</artifactId> <scope>test</scope> </dependency>
Here's a test example:
Production Monitoring and Observability
Once your WebClient calls are in production, you need observability. Use Micrometer to collect metrics on request duration, error rates, and connection pool status. Spring Boot Actuator automatically exposes these metrics if you have the micrometer-registry-prometheus dependency.
Add this to your application.properties: management.endpoints.web.exposure.include=health,metrics,prometheus
To get WebClient-specific metrics, use the .metrics() method on the HttpClient builder. This exposes metrics like http.client.requests (count, duration) and netty.connection.pool.*.
Here's how to enable metrics:
The Great Thread Pool Meltdown of 2023
- Never use blocking HTTP clients in high-concurrency services without a dedicated thread pool.
- Always test under realistic load before production.
- WebClient's non-blocking model prevents thread starvation in I/O-bound services.
curl -w "%{time_connect}" https://api.example.com/healthCheck DNS resolution: nslookup api.example.com| File | Command / Code | Purpose |
|---|---|---|
| WebClientConfig.java | @Configuration | Setting Up WebClient in Spring Boot 3.x |
| WebClientErrorHandling.java | Mono | What the Official Docs Won't Tell You |
| PaymentService.java | @Service | Making GET and POST Requests with WebClient |
| PaymentServiceWithRetry.java | public Mono | Advanced Error Handling and Retry Strategies |
| WebClientConfigProduction.java | @Bean | Timeouts and Connection Pooling Configuration |
| LoggingFilter.java | public class LoggingFilter implements ExchangeFilterFunction { | Exchange Filter Functions for Cross-Cutting Concerns |
| PaymentServiceTest.java | @ExtendWith(MockitoExtension.class) | Testing WebClient with MockWebServer |
| WebClientConfigMetrics.java | @Bean | Production Monitoring and Observability |
Key takeaways
Interview Questions on This Topic
What is WebClient and how does it differ from RestTemplate?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
That's Spring Boot. Mark it forged?
3 min read · try the examples if you haven't