@LoadBalanced with RestClient — the problem and the fix
Spring Boot 4.1.1 · Spring Cloud 2025.1.2 · Eureka
The problem
You add a @LoadBalanced RestClient.Builder, expecting it to work the way @LoadBalanced RestTemplate
always did. Instead your application never registers with Eureka, and the log repeats:
java.lang.IllegalStateException: No instances available for localhost
WARN ... DiscoveryClient : registration failed Cannot execute request on any known server
ERROR ... DiscoveryClient : was unable to send heartbeat!
The application still starts, which is what makes it confusing — it just silently never appears in
the registry.
Why it happens
The Eureka client needs an HTTP client for its own calls to the registry, and it looks for a
RestClient.Builder in your application context:
// DiscoveryClientOptionalArgsConfiguration.RestClientConfiguration
ObjectProvider<RestClient.Builder> restClientBuilderProvider
...
() -> restClientBuilderProvider.getIfAvailable(RestClient::builder);
If the only RestClient.Builder bean you have is the @LoadBalanced one, Eureka finds and uses it.
Its calls to http://localhost:8761/eureka then go through the load balancer, which tries to resolve
localhost as a service id and fails.
Two details make this easy to hit:
LoadBalancerRestClientBuilderBeanPostProcessor has already added the load balancer interceptor to
that builder instance, so anything resolving the bean inherits the interceptor.
- Declaring any
RestClient.Builder bean suppresses Boot's auto-configured one
(RestClientAutoConfiguration#restClientBuilder is @ConditionalOnMissingBean), which removes the
plain fallback candidate and leaves the load-balanced builder as the only match.
The fix
Keep a second, plain @Primary builder so the Eureka transport has an ordinary candidate to find,
and ask for the load-balanced one explicitly by qualifier:
@Configuration
public class RestClientConfig {
/** Plain and @Primary — this is the one the Eureka transport picks up. */
@Bean
@Primary
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public RestClient.Builder restClientBuilder(RestClientBuilderConfigurer configurer) {
return configurer.configure(RestClient.builder());
}
/** Load-balanced — gets the interceptor from the bean post processor. */
@Bean
@LoadBalanced
public RestClient.Builder loadBalancedRestClientBuilder(RestClientBuilderConfigurer configurer) {
return configurer.configure(RestClient.builder());
}
/** Note the @LoadBalanced on the parameter — see rule 2 below. */
@Bean
public RestClient serviceARestClient(@LoadBalanced RestClient.Builder builder) {
return builder.baseUrl("http://SERVICE-A").build();
}
}
With this in place, registration succeeds and requests round-robin correctly across instances.
Two rules to remember
1. Don't delete the plain builder. It looks like dead code. It is the entire fix — it exists so
the Eureka transport has something non-load-balanced to resolve.
2. @LoadBalanced must also be on the injection point.
// WRONG — compiles, runs, quietly does no load balancing
public RestClient serviceARestClient(RestClient.Builder loadBalancedRestClientBuilder) { ... }
// RIGHT
public RestClient serviceARestClient(@LoadBalanced RestClient.Builder builder) { ... }
Naming the parameter after the bean does not select it. DefaultListableBeanFactory#determineAutowireCandidate
checks for a @Primary candidate first and only falls back to parameter-name matching afterwards,
so the plain builder wins. @LoadBalanced is itself a @Qualifier, which is why putting it on the
parameter works.
Why RestTemplate never had this problem
|
@LoadBalanced RestTemplate |
@LoadBalanced RestClient.Builder |
| Load balancing works |
yes |
yes |
| Eureka registration affected |
no |
yes — needs the extra plain builder |
| Why |
Eureka's transport doesn't consume RestTemplate beans |
Eureka's transport resolves RestClient.Builder by type |
WebClient.Builder is resolved the same way as RestClient.Builder, but only when
eureka.client.webclient.enabled=true, so a default setup never reaches that path.
Dependency note
RestClientBuilderConfigurer is org.springframework.boot.restclient.autoconfigure.RestClientBuilderConfigurer
and comes from the spring-boot-restclient module. On Boot 4, spring-boot-starter-webmvc does not
bring it in, so add:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-restclient</artifactId>
</dependency>
Without it there is no auto-configured RestClient.Builder at all, and the snippet above won't compile.
Upstream
Reported in spring-cloud-netflix#4524
and spring-cloud-commons#1633.
Both open at the time of writing — check them before assuming this workaround is still needed.
@LoadBalancedwithRestClient— the problem and the fixSpring Boot 4.1.1 · Spring Cloud 2025.1.2 · Eureka
The problem
You add a
@LoadBalanced RestClient.Builder, expecting it to work the way@LoadBalanced RestTemplatealways did. Instead your application never registers with Eureka, and the log repeats:
The application still starts, which is what makes it confusing — it just silently never appears in
the registry.
Why it happens
The Eureka client needs an HTTP client for its own calls to the registry, and it looks for a
RestClient.Builderin your application context:If the only
RestClient.Builderbean you have is the@LoadBalancedone, Eureka finds and uses it.Its calls to
http://localhost:8761/eurekathen go through the load balancer, which tries to resolvelocalhostas a service id and fails.Two details make this easy to hit:
LoadBalancerRestClientBuilderBeanPostProcessorhas already added the load balancer interceptor tothat builder instance, so anything resolving the bean inherits the interceptor.
RestClient.Builderbean suppresses Boot's auto-configured one(
RestClientAutoConfiguration#restClientBuilderis@ConditionalOnMissingBean), which removes theplain fallback candidate and leaves the load-balanced builder as the only match.
The fix
Keep a second, plain
@Primarybuilder so the Eureka transport has an ordinary candidate to find,and ask for the load-balanced one explicitly by qualifier:
With this in place, registration succeeds and requests round-robin correctly across instances.
Two rules to remember
1. Don't delete the plain builder. It looks like dead code. It is the entire fix — it exists so
the Eureka transport has something non-load-balanced to resolve.
2.
@LoadBalancedmust also be on the injection point.Naming the parameter after the bean does not select it.
DefaultListableBeanFactory#determineAutowireCandidatechecks for a
@Primarycandidate first and only falls back to parameter-name matching afterwards,so the plain builder wins.
@LoadBalancedis itself a@Qualifier, which is why putting it on theparameter works.
Why
RestTemplatenever had this problem@LoadBalanced RestTemplate@LoadBalanced RestClient.BuilderRestTemplatebeansRestClient.Builderby typeWebClient.Builderis resolved the same way asRestClient.Builder, but only wheneureka.client.webclient.enabled=true, so a default setup never reaches that path.Dependency note
RestClientBuilderConfigurerisorg.springframework.boot.restclient.autoconfigure.RestClientBuilderConfigurerand comes from the
spring-boot-restclientmodule. On Boot 4,spring-boot-starter-webmvcdoes notbring it in, so add:
Without it there is no auto-configured
RestClient.Builderat all, and the snippet above won't compile.Upstream
Reported in spring-cloud-netflix#4524
and spring-cloud-commons#1633.
Both open at the time of writing — check them before assuming this workaround is still needed.