HomeSpring BootSpring Cloud Eureka Client: Setup, Config and Load Balancing
Spring Boot

Spring Cloud Eureka Client: Setup, Config and Load Balancing

Set up a Spring Boot Eureka Client: registration, heartbeats, health checks, @LoadBalanced calls, load balancing strategies and instance metadata, with examples.

Eureka Client in 30 Seconds

A Eureka Client is a microservice that registers itself with a Eureka Server and uses the server's registry to find other services. It does four things: 1. Registers: on startup it sends its application name, host, port and health URL to the server. 2. Sends heartbeats: every 30 seconds by default, to prove it is still alive. 3. Fetches the registry: every 30 seconds by default, into a local cache. 4. Deregisters: when the application shuts down gracefully. To turn a Spring Boot service into a Eureka Client you need three things: the spring-cloud-starter-netflix-eureka-client dependency, a spring.application.name, and the Eureka Server URL in eureka.client.service-url.defaultZone. To call another service you use its logical name (for example http://user-service/api/users/1) with a @LoadBalanced RestTemplate or WebClient, and Spring Cloud LoadBalancer picks a real instance from the local cache.

Setting Up a Eureka Client

Add the eureka-client starter to any microservice. Spring Boot auto-configuration registers the service with Eureka on startup and deregisters it on graceful shutdown. The only mandatory configuration is the application name (used as the registry key) and the Eureka Server URL. Spring Cloud dependencies are version-managed by the Spring Cloud BOM, so import it and choose the release train that matches your Spring Boot version (check the compatibility table on the Spring Cloud site). The eureka-client starter already includes Spring Cloud LoadBalancer, so you do not need to add it separately for RestTemplate and WebClient load balancing. If you use WebClient, also add spring-boot-starter-webflux.
XML
<!-- pom.xml: -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>   <!-- match your Spring Boot version -->
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencies>

Eureka Client Application Class

@EnableDiscoveryClient activates registration and discovery. It is optional: since Spring Cloud Edgware (Spring Cloud 2.0 and later), the auto-configuration activates automatically when a discovery client starter is on the classpath. Many teams keep the annotation for explicitness. To disable registration without removing the dependency, set eureka.client.enabled=false or spring.cloud.discovery.enabled=false.
Java
@SpringBootApplication
@EnableDiscoveryClient   // optional: auto-configured when the starter is on the classpath
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

Eureka Client Configuration (application.yml)

The most important settings are the application name, the Eureka Server URL, and the instance identity. The values below are the real defaults for the timing properties, shown explicitly so you can see what you are tuning. Note the key name defaultZone: it is a map key, so it must be written exactly as defaultZone (not default-zone). If the Eureka Server is secured, put credentials in the URL: http://user:password@host:8761/eureka/.
yaml
server:
  port: 8081

spring:
  application:
    name: user-service            # registry key, shown upper-case (USER-SERVICE) on the dashboard

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
    register-with-eureka: true            # default: true
    fetch-registry: true                  # default: true
    registry-fetch-interval-seconds: 30   # default: refresh local cache every 30s

  instance:
    prefer-ip-address: true               # register the IP instead of the hostname
    lease-renewal-interval-in-seconds: 30     # default: heartbeat every 30s
    lease-expiration-duration-in-seconds: 90  # default: server may evict after 90s without heartbeat
    instance-id: ${spring.application.name}:${server.port}
    # Each instance needs a unique id. If you run with server.port=0 (random port),
    # use ${spring.application.name}:${random.value} instead.

# What happens on startup:
# 1. Application starts -> the Eureka registration component fires
# 2. POST /eureka/apps/USER-SERVICE            (registers this instance)
# 3. Server stores: host, port, health URL, status UP, metadata
# 4. Every 30s: PUT /eureka/apps/USER-SERVICE/{instance-id}   (heartbeat)
# 5. On graceful shutdown: DELETE /eureka/apps/USER-SERVICE/{instance-id}

Verify That Registration Works

Do not assume registration worked; check it. There are three quick checks. 1. Client log: after startup, look for a line ending with "registration status: 204". That is the Eureka Server acknowledging the registration. 2. Dashboard: open http://localhost:8761. Your service appears under "Instances currently registered with Eureka" with status UP. 3. REST API: the server exposes the registry as XML or JSON. Ask for JSON with an Accept header. Registration is not instant. A new instance is registered a short while after startup, the server caches responses for a short time, and other clients refresh their own caches on a timer, so it can take a minute or two before every other service can see it. This is normal and is covered in the timing table below.
Shell
# 1. Look in the client log for:
#    DiscoveryClient_USER-SERVICE/user-service:8081 - registration status: 204

# 2. Ask the server for the registry as JSON:
curl -H "Accept: application/json" http://localhost:8761/eureka/apps/USER-SERVICE

# 3. List all registered applications:
curl -H "Accept: application/json" http://localhost:8761/eureka/apps

# 4. Ask the client itself what it can see (via DiscoveryClient):
#    GET http://localhost:8081/debug/instances   (endpoint from the DiscoveryService example below)

Health Checks: Heartbeat Is Not Health

By default a Eureka Client's heartbeat only proves that the process is running and can reach the server. A service whose database is down still sends heartbeats, so it stays marked UP and keeps receiving traffic. To fix this, enable health check propagation. The client then sends its Spring Boot Actuator health status to Eureka. When the application's health becomes DOWN, the instance's status in Eureka changes to DOWN, and Eureka stops handing that instance out for discovery until it recovers. Any HealthIndicator you write (database, message broker, downstream dependency) feeds into this status. Also set the health and status page URLs if you use a custom management port or context path, so that Eureka stores correct links.
yaml
# application.yml
eureka:
  client:
    healthcheck:
      enabled: true                       # propagate Actuator health to Eureka
  instance:
    health-check-url-path: /actuator/health
    status-page-url-path: /actuator/info

management:
  endpoints:
    web:
      exposure:
        include: health,info
  endpoint:
    health:
      show-details: when-authorized       # avoid exposing details publicly

// ── Custom health indicator that can mark the instance DOWN: ──────────
@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final PaymentGatewayClient gateway;

    public PaymentGatewayHealthIndicator(PaymentGatewayClient gateway) {
        this.gateway = gateway;
    }

    @Override
    public Health health() {
        return gateway.isReachable()
            ? Health.up().build()
            : Health.down().withDetail("gateway", "unreachable").build();
        // DOWN here -> Eureka status DOWN after the next status update
    }
}

Calling Other Services via Eureka

Once registered, a service can call peers using their logical name. Spring Cloud LoadBalancer (included with the Eureka client starter) intercepts calls made with a @LoadBalanced RestTemplate or WebClient.Builder and resolves the service name to a real instance from the local registry cache. The three options below are alternatives, shown in separate classes; do not put them all in one application unless you give the beans different names. Important: a @LoadBalanced RestTemplate treats every host name as a service name. If you also need to call an external URL such as https://api.example.com, use a separate, plain RestTemplate (with a @Qualifier), or the call fails with "No instances available for api.example.com". Also avoid underscores in spring.application.name. A URL such as http://user_service/ is not a valid host name for java.net.URI and the host resolves to null. Use hyphens: user-service.
Java
// ── Option 1: @LoadBalanced RestTemplate ─────────────────────────────
@Configuration
public class AppConfig {

    @Bean
    @LoadBalanced   // instructs Spring Cloud LoadBalancer to intercept
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Bean
    @Qualifier("external")   // plain RestTemplate for non-Eureka URLs
    public RestTemplate externalRestTemplate() {
        return new RestTemplate();
    }
}

@Service
@RequiredArgsConstructor
public class OrderService {

    private final RestTemplate restTemplate;   // the @LoadBalanced one (only unqualified bean of this type)

    public UserResponse getUser(Long id) {
        // "user-service" is the spring.application.name of the target.
        // LoadBalancer resolves it to a real host:port via Eureka.
        return restTemplate.getForObject(
            "http://user-service/api/users/" + id,
            UserResponse.class
        );
    }
}

// ── Option 2: @LoadBalanced WebClient (non-blocking, needs spring-boot-starter-webflux): ──
@Configuration
public class WebClientConfig {

    @Bean
    @LoadBalanced
    public WebClient.Builder webClientBuilder() {
        return WebClient.builder();
    }
}

@Service
@RequiredArgsConstructor
public class OrderReactiveService {

    private final WebClient.Builder webClientBuilder;

    public Mono<UserResponse> getUser(Long id) {
        return webClientBuilder.build()
            .get()
            .uri("http://user-service/api/users/" + id)
            .retrieve()
            .bodyToMono(UserResponse.class);
    }
}

// ── Option 3: DiscoveryClient — manual instance resolution: ───────────
@Service
@RequiredArgsConstructor
public class DiscoveryService {

    private final DiscoveryClient discoveryClient;

    public List<String> listInstances() {
        List<ServiceInstance> instances =
            discoveryClient.getInstances("user-service");

        return instances.stream()
            .map(i -> i.getInstanceId() + " -> " + i.getUri())
            .toList();
    }
}

@RestController
@RequiredArgsConstructor
class DiscoveryDebugController {
    private final DiscoveryService discoveryService;

    @GetMapping("/debug/instances")
    public List<String> instances() { return discoveryService.listInstances(); }
}

Load Balancing Strategies

Spring Cloud LoadBalancer ships with two built-in strategies: RoundRobinLoadBalancer (the default, distributes requests evenly) and RandomLoadBalancer. Custom strategies implement ReactorServiceInstanceLoadBalancer. The strategy can be configured per target service with @LoadBalancerClient, or for every service with @LoadBalancerClients(defaultConfiguration = ...). One pitfall: the configuration class you pass to @LoadBalancerClient (here RandomLBConfig) must NOT be annotated with @Configuration and must not be picked up by component scanning. If it is, it applies to all services instead of only the named one. Zone preference does not need custom code. Spring Cloud LoadBalancer has a built-in zone-preference supplier: set spring.cloud.loadbalancer.zone on the caller, publish a zone entry in each target instance's Eureka metadata, and enable the zone-preference configuration. A custom load balancer is only needed for rules beyond that.
Java
// ── Default: RoundRobinLoadBalancer ──────────────────────────────────
// Requests to "user-service" are sent to instances in rotation:
//   Request 1 -> 172.18.0.5:8081
//   Request 2 -> 172.18.0.6:8081
//   Request 3 -> 172.18.0.5:8081  (back to first)

// ── Override to RandomLoadBalancer for a specific service: ────────────
@Configuration
@LoadBalancerClient(
    name = "user-service",           // applies only to user-service calls
    configuration = RandomLBConfig.class
)
public class AppLoadBalancerConfig { }

// NOTE: no @Configuration here, and keep it outside component-scanned packages
public class RandomLBConfig {

    @Bean
    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
            Environment env,
            LoadBalancerClientFactory factory) {
        String name = env.getProperty(
            LoadBalancerClientFactory.PROPERTY_NAME);
        return new RandomLoadBalancer(
            factory.getLazyProvider(name,
                ServiceInstanceListSupplier.class),
            name
        );
    }
}

// ── Override globally (all services): ────────────────────────────────
@Configuration
@LoadBalancerClients(defaultConfiguration = RandomLBConfig.class)
public class GlobalLoadBalancerConfig { }

// ── Built-in zone preference (no code): application.yml of the CALLER ─
// spring:
//   cloud:
//     loadbalancer:
//       zone: us-east-1a
//       configurations: zone-preference
//
// and in each TARGET instance:
// eureka:
//   instance:
//     metadata-map:
//       zone: us-east-1a

// ── Custom load balancer (only for rules the built-ins cannot express) ─
public class ZoneAwareLoadBalancer implements
        ReactorServiceInstanceLoadBalancer {

    private final ObjectProvider<ServiceInstanceListSupplier> supplier;
    private final String serviceId;
    private final String currentZone;

    public ZoneAwareLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> supplier,
                                 String serviceId,
                                 String currentZone) {
        this.supplier = supplier;
        this.serviceId = serviceId;
        this.currentZone = currentZone;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        return supplier.getIfAvailable(NoopServiceInstanceListSupplier::new)
            .get(request)
            .next()
            .map(instances -> {
                if (instances.isEmpty()) {
                    return new EmptyResponse();
                }
                List<ServiceInstance> sameZone = instances.stream()
                    .filter(i -> currentZone.equals(
                        i.getMetadata().get("zone")))
                    .toList();
                List<ServiceInstance> pool =
                    sameZone.isEmpty() ? instances : sameZone;
                return new DefaultResponse(
                    pool.get(ThreadLocalRandom.current()
                        .nextInt(pool.size())));
            });
    }
}

Instance Metadata and Zone Awareness

Each Eureka instance can publish arbitrary key-value metadata alongside its registration. Metadata is visible on the Eureka dashboard and accessible to other services through ServiceInstance.getMetadata(). Common uses include zone tagging, version labels for canary deployments, ownership tags and custom weights. The simplest and safest way to publish metadata is the eureka.instance.metadata-map property. Values must be strings, so quote numbers. To publish the build version, let Maven fill it in at build time with @project.version@ (requires resource filtering, which spring-boot-starter-parent enables for application.yml). If you need to add metadata at runtime, use ApplicationInfoManager.registerAppMetadata(...). It updates the instance info and the change is sent to the server with the next replication. Avoid declaring your own EurekaInstanceConfigBean bean to add metadata: it replaces the auto-configured bean and you lose the values bound from eureka.instance.* properties unless you rebind them yourself. Do not confuse the metadata zone (used by Spring Cloud LoadBalancer to prefer nearby service instances) with eureka.client.availability-zones, which controls which Eureka Server a client prefers.
yaml
# ── Publishing metadata from a client instance (application.yml): ────
eureka:
  instance:
    metadata-map:
      zone: us-east-1a          # used by zone-preference load balancing
      version: "@project.version@"   # filled in by Maven at build time
      team: payments            # ownership tag
      weight: "80"              # custom traffic weight for canary (strings only)

// ── Reading metadata in another service: ──────────────────────────────
@Service
@RequiredArgsConstructor
public class MetadataAwareService {

    private final DiscoveryClient discoveryClient;

    public Optional<ServiceInstance> findCanaryInstance(String serviceName) {
        return discoveryClient.getInstances(serviceName).stream()
            .filter(i -> "canary".equals(i.getMetadata().get("track")))
            .findFirst();
    }

    public void printAllMetadata(String serviceName) {
        discoveryClient.getInstances(serviceName).forEach(instance -> {
            System.out.println("Instance: " + instance.getInstanceId());
            instance.getMetadata()
                .forEach((k, v) -> System.out.println("  " + k + "=" + v));
        });
    }
}

// ── Adding metadata at runtime (after the application is ready): ─────
@Component
@RequiredArgsConstructor
public class RuntimeMetadataPublisher {

    private final ApplicationInfoManager applicationInfoManager;

    @EventListener(ApplicationReadyEvent.class)
    public void publish() {
        applicationInfoManager.registerAppMetadata(Map.of(
            "startedAt", Instant.now().toString(),
            "feature.search", "enabled"
        ));
        // sent to the Eureka Server with the next instance info replication
    }
}

Timing and Caching: Why Changes Are Not Instant

Eureka is designed to favour availability over strict consistency, so there are several layers of caching between a service starting up and another service calling it. Knowing the defaults explains most "my service is registered but not found" questions. Default timings: - Client heartbeat (lease renewal): every 30 seconds - Lease expiration: 90 seconds without a heartbeat - Initial registration replication after startup: about 40 seconds - Eureka Server read-only response cache refresh: every 30 seconds - Client registry fetch: every 30 seconds - Spring Cloud LoadBalancer instance cache: about 35 seconds - Server eviction task: runs every 60 seconds A new instance can therefore take a minute or two to become visible everywhere, and a crashed instance can keep receiving traffic for longer than the 90-second lease suggests. Clients should be written to tolerate this: use timeouts, retries and a circuit breaker such as Resilience4j rather than assuming the registry is always exact. In development you can shorten these values (for example, lower the fetch interval and server cache interval) but keep the defaults in production unless you have measured a reason to change them.

Common Eureka Client Problems

1. Service shows up as UNKNOWN. spring.application.name is missing. Set it before anything else. 2. Client does not register. Check that eureka.client.service-url.defaultZone is spelled exactly like that (camel case), that register-with-eureka is not false, and that the server URL is reachable from the client (inside Docker, localhost is the container itself). 3. UnknownHostException: user-service. The RestTemplate is not @LoadBalanced, or you injected a different RestTemplate bean than the load-balanced one. 4. "No instances available for xyz". The target is not registered yet (see the timing section), the name is wrong (names are case-insensitive but must match), the target is DOWN, or you are calling an external host through the load-balanced RestTemplate. 5. Other services cannot reach the instance. The client registered an unreachable hostname. In Docker or Kubernetes set eureka.instance.prefer-ip-address=true, or set eureka.instance.hostname and eureka.instance.ip-address explicitly. 6. Several instances overwrite each other. Instances on the same host with the same instance-id replace one another. Make the id unique, for example with ${random.value} when using server.port=0. 7. Dead instances stay in the registry. A crashed or kill -9'd process cannot deregister; it disappears only after its lease expires. Use graceful shutdown (SIGTERM) so the DELETE call is sent. If many instances vanish at once, the server may be in self-preservation mode and will keep them on purpose. 8. Instance is UP but its dependencies are broken. Heartbeats do not check health. Enable eureka.client.healthcheck.enabled=true. 9. Underscores in the service name. Use hyphens; a host name with an underscore breaks URI parsing.

Frequently Asked Questions

Is @EnableDiscoveryClient required? No. Since Spring Cloud Edgware (Spring Cloud 2.0 and later), registration is auto-configured when a discovery client starter such as the Eureka client is on the classpath. The annotation is harmless and makes the intent explicit. What is the difference between a Eureka Client and a Eureka Server? The server holds the registry. Clients register with it, send heartbeats and read the registry. Most microservices are clients; you normally run one server cluster. How often does a Eureka Client send heartbeats? Every 30 seconds by default (lease-renewal-interval-in-seconds). The server considers the instance expired after 90 seconds without a heartbeat (lease-expiration-duration-in-seconds). How does a Eureka Client find other services? It fetches the registry from the server into a local cache every 30 seconds and resolves service names from that cache, so lookups do not call the server on each request. Which load balancer does Eureka use? Spring Cloud LoadBalancer, with round robin by default. Netflix Ribbon is no longer used in current Spring Cloud versions. Can a service be a client without registering itself? Yes. Set eureka.client.register-with-eureka=false and keep fetch-registry=true. This is useful for consumers such as an API client or a batch job that only call other services. Can I use Eureka without Spring Boot's @LoadBalanced? Yes. Inject DiscoveryClient (or EurekaClient) and pick an instance yourself, as shown in the DiscoveryClient example. How do I run several instances locally? Start the application multiple times with different ports (--server.port=8082) or with server.port=0, and make sure instance-id is unique per instance. Why does a stopped service still show in the dashboard? Because its lease has not expired yet, or the server is in self-preservation mode. A graceful shutdown removes it immediately. Do I still need Eureka on Kubernetes? Not necessarily. Kubernetes provides its own service discovery and load balancing. Eureka is most useful on VMs, in hybrid setups, or when you already depend on the Spring Cloud ecosystem. Spring Cloud Kubernetes is an alternative if you stay on Kubernetes.

Key Takeaways

- A Eureka Client needs the starter, spring.application.name and eureka.client.service-url.defaultZone; @EnableDiscoveryClient is optional. - Defaults: heartbeat every 30s, lease expiry 90s, registry fetch every 30s. Expect up to a minute or two before a new instance is visible everywhere. - Heartbeats do not prove health; enable eureka.client.healthcheck.enabled=true so Actuator health reaches Eureka. - Call services by name with a @LoadBalanced RestTemplate or WebClient.Builder, and keep a separate plain RestTemplate for external URLs. - Round robin is the default load balancer; use zone-preference and metadata before writing a custom one. - Make instance ids unique, prefer IPs in containers, use hyphens in service names, and shut down gracefully. - Related topics: Eureka Server, Spring Cloud Gateway, Resilience4j Circuit Breaker, OpenFeign.