Mastering WebClient Retries in Spring Boot 3: From Basics to Circuit Breaker
This article explains how to use Spring WebFlux's WebClient for HTTP calls, demonstrates basic retry, backoff, custom retry logic, jitter to avoid retry storms, and integrates circuit breakers with Resilience4j to build resilient Spring Boot 3 applications.
1. Introduction
Spring WebFlux provides WebClient for HTTP requests. It offers a reactive, non‑blocking API that can compose asynchronous logic without dealing with threads.
In distributed systems, network calls may fail; retry mechanisms improve resilience. RestTemplate and RestClient require custom interceptors or Spring Retry, while WebClient has built‑in reactive retry operators like retryWhen and RetryBackoffSpec.
2. Practical Examples
2.1 Basic Usage
WebClient does not retry by default. Use .retry(n) to retry n times after a failure.
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000);
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:9999")
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
webClient.get()
.uri("/api/query")
.retrieve()
.bodyToMono(String.class)
.doOnError(err -> System.err.printf("Error: %s%n", err.getMessage()))
.retry(2)
.subscribe(System.out::println, error -> System.err.printf("Request failed: %s%n", error.getMessage()));This retries immediately, which may overload the target service.
2.2 Adding Backoff
Use Retry.backoff with retryWhen to introduce a delay that can increase on each attempt.
webClient.get()
.uri("/api/query")
.retrieve()
.bodyToMono(String.class)
.doOnError(err -> System.err.printf("Error at %s: %s%n",
DateTimeFormatter.ofPattern("mm:ss").format(LocalDateTime.now()), err.getMessage()))
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
.subscribe(System.out::println, error -> System.err.printf("Request failed: %s%n", error.getMessage()));2.3 Custom Retry Logic
Filter which status codes should be retried (e.g., retry only 5xx or timeout errors).
Retry retryStrategy = Retry.fixedDelay(2, Duration.ofMillis(500))
.filter(throwable -> {
if (throwable instanceof WebClientResponseException) {
int status = ((WebClientResponseException) throwable).getStatusCode().value();
return status >= 500;
}
return throwable instanceof WebClientRequestException;
});
webClient.get()
.uri("/api/query")
.retrieve()
.bodyToMono(String.class)
.doOnError(err -> System.err.printf("Error at %s: %s%n",
DateTimeFormatter.ofPattern("mm:ss").format(LocalDateTime.now()), err.getMessage()))
.retryWhen(retryStrategy)
.subscribe(System.out::println, error -> System.err.printf("Request failed: %s%n", error.getMessage()));2.4 Avoiding Retry Storms
Combine backoff with jitter to randomize delays and prevent synchronized retries.
Retry retryWithJitter = Retry.backoff(4, Duration.ofMillis(500))
.jitter(0.8)
.filter(throwable -> {
if (throwable instanceof WebClientResponseException) {
int status = ((WebClientResponseException) throwable).getStatusCode().value();
return status >= 500;
}
return throwable instanceof WebClientRequestException;
});
webClient.get()
.uri("/api/query")
.retrieve()
.bodyToMono(String.class)
.doOnError(err -> System.err.printf("Error at %s: %s%n",
DateTimeFormatter.ofPattern("mm:ss").format(LocalDateTime.now()), err.getMessage()))
.retryWhen(retryWithJitter)
.subscribe(System.out::println, error -> System.err.printf("Request failed: %s%n", error.getMessage()));2.5 Fallback with Circuit Breaker
If retries fail, a circuit breaker can stop further calls. Spring Cloud Circuit Breaker or Resilience4j can be integrated.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
<version>3.3.0</version>
</dependency>Configuration example (YAML):
resilience4j:
circuitbreaker:
configs:
order-service:
minimum-number-of-calls: 1
failure-rate-threshold: 10
wait-duration-in-open-state: 10sSample code using ReactiveCircuitBreakerFactory:
private final ReactiveCircuitBreakerFactory<?, ?> rcbFactory;
public Mono<String> invoke() {
return rcbFactory.create("order-service")
.run(webClient.get()
.uri("/api/query")
.retrieve()
.bodyToMono(String.class)
.doOnError(err -> System.err.printf("Error at %s: %s%n",
DateTimeFormatter.ofPattern("mm:ss").format(LocalDateTime.now()), err.getMessage())),
ex -> Mono.just("fallback response"));
}These patterns help build resilient Spring Boot 3 applications that handle transient failures gracefully.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
