Skip to content

@LoadBalanced with RestClient` — the problem and the fix #4591

Description

@hayattarique

@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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions