Service Discovery Using Eureka Server: How It Actually Works
Eureka is a REST-based registry: each service registers its host and port with the Eureka Server, heartbeats every 30 seconds to keep the lease alive, and pulls a cached copy of the registry every 30 seconds. Callers then use a service ID like http://order-service/orders, which Spring Cloud LoadBalancer resolves to a real instance.
The problem Eureka exists to solve
You have an order service that needs to call an inventory service. In a single-server world you write http://10.0.4.12:8080/inventory into a config file and move on. That breaks the moment instances become disposable: autoscaling adds a third inventory instance at a new IP, a container restart gives it a different port, a deploy replaces all of them. The address you hardcoded is a fact with a shelf life of hours.
Service discovery replaces the hardcoded address with a name. The order service asks "where is inventory-service?" and gets back a live list of host/port pairs. There are two ways to arrange that:
- Server-side discovery — the caller sends every request to a proxy or load balancer, and the proxy does the lookup. The caller knows nothing.
- Client-side discovery — the caller itself holds a copy of the registry and picks an instance before it opens the connection.
Eureka is the client-side model. It was built at Netflix to locate middle-tier services in AWS for load balancing and failover, and it is deliberately just a REST API over an in-memory map. Three moving parts:
| Part | What it does |
|---|---|
| Eureka Server | Holds the registry (in memory) and answers HTTP calls under /eureka/ |
| Eureka Client | A library inside every service; registers itself, heartbeats, and pulls a local copy of the registry |
| Load balancer | Spring Cloud LoadBalancer turns http://inventory-service/items into http://10.0.4.12:8080/items |
Eureka is an AP system in CAP terms: during a network partition it keeps answering with possibly-stale data rather than refusing to answer. That single design decision explains almost every surprising behaviour in this article.
Note
Spring Cloud Netflix once shipped Ribbon, Hystrix, Zuul 1 and Archaius. Those went into maintenance mode with the Greenwich release, and Ribbon has since been removed from Spring Cloud. Eureka is the one that stayed, and it is still shipped and supported.
The protocol: register, renew, fetch, cancel, evict
Everything Eureka does is four HTTP operations plus a background sweep on the server.
Register. On startup the client POSTs an InstanceInfo object to /eureka/apps/{appName} — application name, instance id, hostname, IP, port, status-page and health-check URLs, and a metadata map. The server drops it into the registry with a lease.
Renew. The client sends a heartbeat every 30 seconds (eureka.instance.lease-renewal-interval-in-seconds, from LeaseInfo.DEFAULT_LEASE_RENEWAL_INTERVAL = 30). Missing heartbeats is the only way the server learns an instance died unexpectedly.
Lease expiry. If no heartbeat arrives for 90 seconds (eureka.instance.lease-expiration-duration-in-seconds, DEFAULT_LEASE_DURATION = 90), the lease is considered expired.
Eviction. Expired leases are not removed the instant they expire. An EvictionTask sweeps the registry on a timer, default 60000 ms (eureka.server.eviction-interval-timer-in-ms).
Fetch registry. Each client pulls the whole registry once, then polls deltas every 30 seconds (eureka.client.registry-fetch-interval-seconds) and keeps the result in a local cache. Because the cache is local, your services keep resolving each other for a while even if every Eureka server is down.
Cancel. A graceful shutdown sends DELETE /eureka/apps/{appName}/{instanceId} so the server drops the instance immediately instead of waiting out the lease.
One more delay hides on the server: reads are served from a read-only response cache refreshed from the read-write registry every 30000 ms (eureka.server.response-cache-update-interval-ms).
Worked example: you kill -9 an instance. Who notices, when?
Two instances of inventory-service, defaults everywhere. At T+0 you kill -9 instance B — no cancel is sent, because the JVM never ran a shutdown hook.
| Time | What happens |
|---|---|
| T+0 | B dies. Server registry still says B is UP. Every client cache says B is UP. |
| T+0 → T+90 | Server waits for a heartbeat that never comes. Lease not yet expired. |
| T+90 | Lease is now expired — but nothing has removed it. |
| T+90 → T+150 | Next EvictionTask tick (up to 60s away) removes B from the read-write registry. |
| + up to 30s | Read-only response cache refreshes, so fetching clients can finally see the removal. |
| + up to 30s | Each client's next registry fetch updates its local cache. |
Worst case is at least 90 + 60 + 30 + 30 seconds before the last caller stops trying B — and in practice a little more, because the 90-second clock runs from the last heartbeat the server actually received, which may be up to 30 seconds before the process died. For the whole of that window, a @LoadBalanced RestTemplate will cheerfully round-robin requests into a dead socket. This is not a bug; it is the AP trade-off billed in seconds. Discovery is not a failover mechanism — the retry and circuit-breaker layer is.
Instances carry a status of UP, DOWN, STARTING, OUT_OF_SERVICE or UNKNOWN, and traffic normally goes only to UP ones.
Standing up a Eureka Server
Add the server starter (with the spring-cloud-dependencies BOM managing versions) and one annotation.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
package com.example.registry;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class RegistryApplication {
public static void main(String[] args) {
SpringApplication.run(RegistryApplication.class, args);
}
}
@EnableEurekaServer is what pulls in the registry, the replication machinery and the dashboard — unlike on the client side, this annotation is still required.
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
Port 8761 is the conventional Spring Cloud port, and it matters: the Eureka client's default eureka.client.service-url.defaultZone is http://localhost:8761/eureka/, so matching it means clients need no URL config at all locally. The two false flags matter too — a Eureka server is itself a Eureka client, and on a single node these flags stop it trying to register with or fetch from a peer that does not exist.
Start it and open http://localhost:8761/. The server serves an HTML dashboard at the root path listing registered applications and instance status, and the machine-readable registry sits under /eureka/ — curl http://localhost:8761/eureka/apps gives you the raw view.
For high availability, run two servers that each list the other in service-url.defaultZone; registrations replicate peer-to-peer, so either node can answer. Worth understanding what a single node actually costs you: because clients cache the registry, losing the only server does not immediately break existing routing — it breaks new registrations and changes becoming visible.
Registering a service and calling it by name
On the calling and called services, add the client starter:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
That is the whole wiring. Having the starter on the classpath is enough — in current Spring Cloud you do not need @EnableDiscoveryClient or @EnableEurekaClient. Tutorials that insist on them are describing an older requirement.
spring:
application:
name: inventory-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
spring.application.name becomes the service ID — the name in the dashboard and the name other services look up. prefer-ip-address: true registers the IP instead of the hostname, which is the standard fix in Docker, where a container hostname means nothing to a caller in another container; if you are new to that boundary, our Docker vs Kubernetes piece covers where container networking sits. You can switch the client off wholesale with eureka.client.enabled=false.
To call it, mark a RestTemplate as load-balanced. The bean has to live somewhere Spring will scan — a @Configuration class, or the main @SpringBootApplication class:
package com.example.order;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class LoadBalancerConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Inject that bean where you need it, and call the service by ID:
package com.example.order;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class InventoryCaller {
private final RestTemplate restTemplate;
public InventoryCaller(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public String item() {
return restTemplate.getForObject(
"http://inventory-service/items/42", String.class);
}
}
The host inventory-service is not DNS. Spring Cloud LoadBalancer intercepts the call, looks the service ID up in the client's cached registry, picks an instance (default strategy: round-robin) and rewrites the URI. WebClient.Builder works the same way with @LoadBalanced, and OpenFeign resolves @FeignClient(name = "inventory-service") through the same path. Ribbon used to do this job and is gone.
When you want the raw list rather than a call, inject DiscoveryClient and use getInstances("inventory-service"), which returns ServiceInstance objects with host, port, URI, secure flag and metadata. That interface is identical for Eureka, Consul and Zookeeper, so swapping registries does not rewrite your code.
Self-preservation: the red banner
Sooner or later your dashboard turns red:
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT.
RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED
JUST TO BE SAFE.
Self-preservation means the server counted the heartbeats it received in the last minute, found the number below the expected threshold, and stopped evicting leases entirely. Its reasoning: if most instances went quiet at once, the likelier explanation is that the network between the server and the fleet broke, not that the fleet died. Evicting in that situation would empty the registry and take down a system that is actually healthy — exactly the failure Eureka's AP design is built to avoid.
The threshold comes from eureka.server.renewal-percent-threshold, default 0.85, recomputed every 900000 ms (eureka.server.renewal-threshold-update-interval-ms). With a 30-second heartbeat, each instance is expected to renew twice a minute.
Which is why it is easy to trip in development. On a registry with only a service or two, killing one instance can drop the renewals received in the last minute well below 85% of what the server expects, and the server then stops expiring leases — so your killed instance keeps showing as UP on the dashboard. In dev, that is what eureka.server.enable-self-preservation=false is for. In production it is a safety net for large fleets where an 85% renewal drop really does mean a partition — do not switch it off reflexively.
Tuning, and what it costs
| Knob | Lowering it buys | Lowering it costs |
|---|---|---|
lease-renewal-interval-in-seconds |
Server learns of death sooner | More heartbeat requests per instance per minute |
lease-expiration-duration-in-seconds |
Shorter expiry window | Brief GC pauses or network blips now look like death |
eviction-interval-timer-in-ms |
Expired leases removed sooner | More frequent full-registry sweeps |
registry-fetch-interval-seconds |
Client caches go stale for less time | More polling traffic, multiplied by instance count |
Netflix's defaults are tuned for a large fleet where heartbeat volume is the scarce resource, not for a three-service demo where you want a dead instance gone in five seconds. Tune them for your fleet size, and accept the traffic.
The more important conclusion is architectural: because the registry is eventually consistent, it will hand your caller a dead instance. Pair discovery with client-side retry and circuit breaking so the multi-minute stale window degrades a few requests rather than a feature — the same reasoning that makes consumers in Kafka consumer group rebalances treat membership as something that changes under them.
When not to use Eureka at all
If you are deploying to Kubernetes, a Service already gives you a stable name and virtual IP with cluster DNS resolving it, and readiness probes already gate traffic. Running Eureka on top means maintaining a second, application-level registry that the platform already makes largely redundant. Consul is another registry in this space, with health-check-driven membership and both DNS and HTTP interfaces. Eureka earns its place on VM or plain-container deployments — often on EC2-style infrastructure — where there is no platform registry to lean on and the client-side cache surviving a registry outage is a genuine feature.
If you need a JDK before any of this, see installing Java 21 on Windows.
Frequently asked questions
- Why does my stopped service still show as UP in the Eureka dashboard?
- Because nothing removes it immediately. If the process was killed without a graceful shutdown it never sent a cancel, so the server waits 90 seconds for the lease to expire, then waits for the next EvictionTask tick (every 60 seconds by default) to actually remove it. On top of that the read-only response cache refreshes every 30 seconds. A graceful shutdown avoids all of it by sending DELETE /eureka/apps/{appName}/{instanceId}.
- Do I still need @EnableEurekaClient or @EnableDiscoveryClient?
- No. In current Spring Cloud, putting spring-cloud-starter-netflix-eureka-client on the classpath is enough — the application registers automatically, and neither annotation is required. @EnableEurekaServer is still required on the server side.
- Is it safe to set eureka.server.enable-self-preservation=false?
- In local development and CI, yes — with only a couple of registered services, killing one can drop renewals below the 85% threshold and freeze the registry, which makes testing failover awkward. In production, leave it on: it is a safety net, because with a large fleet a sudden renewal drop is more likely to be a network partition than mass instance death, and disabling it lets the server evict instances that are actually healthy but temporarily unreachable.
- Why is my load-balanced RestTemplate still calling an instance that is down?
- Because the client is reading from its own cached copy of the registry, refreshed every 30 seconds, and that copy comes from a server response cache that itself refreshes every 30 seconds — after a 90-second lease expiry and a 60-second eviction sweep. Discovery is eventually consistent by design. Add retry and circuit breaking so calls to a stale instance fail fast and get retried elsewhere.
- Can services keep talking to each other if the Eureka server goes down?
- Yes, for a while. Each client holds a local cache of the registry, so it can keep resolving service IDs to hosts and ports without the server. What stops working is change: new instances cannot register, and removals and status changes stop propagating, so the cache slowly drifts from reality.
- Should I choose Eureka, Consul, or Kubernetes Services?
- On Kubernetes, Services plus cluster DNS already provide discovery, and readiness probes gate traffic — an application-level registry is usually redundant. Consul is a health-check-driven registry with DNS and HTTP interfaces, leaning towards consistency. Eureka fits VM or plain-container fleets, especially where you value clients being able to route from cache during a registry outage.
References
- Spring Cloud Netflix Reference Documentation — Service Discovery: Eureka Clients / Eureka ServerSpring Cloud Netflix
- Spring Cloud Netflix — Service Discovery: Eureka ServerSpring Cloud Netflix (Eureka Server section)
- Spring Cloud Netflix — Service Discovery: Eureka ClientsSpring Cloud Netflix (Eureka Clients section)
- Eureka at a glance / Understanding Eureka Peer to Peer CommunicationNetflix Eureka wiki
- Netflix/eureka — EurekaServerConfigBean / EurekaInstanceConfigBean / LeaseInfo defaultsNetflix Eureka source
- Spring Cloud Commons — Spring Cloud LoadBalancerSpring Cloud LoadBalancer
- Spring Cloud Commons — Common Abstractions: DiscoveryClient, @EnableDiscoveryClientSpring Cloud Commons
- Spring Cloud — release trains and project statusSpring Cloud project page
- Spring Cloud Greenwich.RELEASE Is Now Available (Netflix projects entering maintenance mode)Spring Blog
- Spring Cloud OpenFeign Reference DocumentationSpring Cloud OpenFeign
- Consul — Service Discovery documentationHashiCorp Consul
- Kubernetes — ServiceKubernetes