Knowledge Guide
HomeSystem DesignMicroservices Patterns

Service Discovery & Distributed Circuit Breakers — Detection Lag, CAP Stance, Registry Herd & Per-Instance vs Coordinated State (Deep Dive)

Why detection lag and distributed breaker state get their own page

A service registry and a circuit breaker both exist to answer the same question — "is this dependency still good to call, right now?" — but neither answer is instant, and neither is shared. The registry's answer is stale by exactly the sum of its lease timers plus your client's cache window: a number you can derive, not guess. The breaker's answer is worse than stale — it is local: N different callers each form their own independent opinion, with no coordination between them, so "the circuit is open" is really N circuits, open or closed on N different schedules. This page derives both delays with real numbers and shows what a senior engineer changes because of them.

The three-state breaker itself (Closed → Open → Half-Open) is covered in "Circuit Breaker — The State Machine (Closed → Open → Half-Open)" — read that first if the state machine is new to you; this page assumes it and goes distributed. Likewise, registration, discovery styles, and the basic staleness window are covered in "What is Service Discovery Pattern" and its worked example — this page owns the exact arithmetic of that staleness, the registry's own CAP choice, the herd problem discovery creates at scale, and how a fleet of circuit breakers actually behaves once it is more than one instance.

1. Detection lag: how long does a dead address keep getting called?

A caller only stops dialing a dead instance once every layer between it and the truth has caught up: the registry must decide the lease is gone, its eviction process must actually act on that decision, and the caller's own cache must refresh. Each layer runs on its own clock, and the delays add, they don't overlap — that's the whole mechanism.

Take Eureka's real defaults, the most commonly interviewed case:

Add the worst case of each stage — they compound, they do not overlap:

StageWorst-case delayRunning total
Lease survives after death (30s renew × up to 2 missed renewals tolerated)90s90s
Eviction sweep hasn't run yet+60s150s
Caller's local cache hasn't refreshed yet+30s180s

Worst case ≈ 90 + 60 + 30 = 180 seconds — a full three minutes before the last caller in the fleet has forgotten a dead address, purely from registry mechanics. Even the typical case — averaging out where in each window the death happened to land — is still on the order of a couple of minutes, because the three delays are independent and rarely all land at their best case simultaneously.

That number is the whole argument for defense in depth: no registry tuning alone gets this under a second. Shrinking the TTLs trades lag for false evictions (a slow GC pause now looks like death) and for heartbeat load on the registry (see §3). Production systems instead bridge the gap at the request layer — independent of the registry's own clock: a client-side connect/read timeout well under a user-visible SLA, an immediate retry against a different instance on failure, and a per-instance circuit breaker that stops routing to a specific dead address within one or two failed calls, long before its lease would ever expire. The registry's job is eventual truth; the breaker's job is to not wait for it.

Detection lag timeline: 90s lease duration plus 60s eviction sweep plus 30s client cache equals a 180 second worst case
Detection lag timeline: 90s lease duration plus 60s eviction sweep plus 30s client cache equals a 180 second worst case

2. AP or CP: the registry has to pick a side of CAP

During a network partition, a registry node that can't reach its peers has exactly two honest options: keep answering with what it has (possibly stale or incomplete), or refuse to answer until it can prove its data is current. That single decision is the registry's CAP stance, and different registries commit to it deliberately, not by accident.

AP — EurekaCP — Consul / etcd / ZooKeeper
During a partitionKeeps serving its local, possibly-stale instance list — every node answers from what it last knewRefuses reads/writes on the minority side unless it can reach quorum — may return no answer at all
Failure modeYou might route to an instance that already died, or miss one that just joined, for as long as the partition lastsDiscovery goes dark for callers stuck on the minority side, even though most of those instances are perfectly healthy
MechanismPeer-to-peer replication, best-effort; self-preservation mode further biases toward "keep serving" over "evict aggressively" when heartbeats look abnormalRaft/ZAB consensus — a write (and often a strongly-consistent read) needs a majority of nodes to agree
Right call whenThe registry's only job is "give me a good-enough address to call" — a stale entry costs one retry, not correctnessThe same store is also arbitrating leader election, distributed locks, or config that must never show two callers two different "truths" at once

The intuition an interviewer wants: for pure service discovery, a slightly stale instance list is nearly always the safer failure — the caller retries a dead address once and moves on. A registry that goes silent during exactly the kind of network event that also makes services flaky is often the worse outcome, because now nobody can find anybody. That's why Eureka's designers explicitly chose AP for a system whose only consumer is "who do I call next." Consul/etcd/ZooKeeper earn their CP stance when the same coordination service is also being trusted for things where two different answers is a correctness bug, not an inconvenience.

3. Registry-side thundering herd: poll vs watch

The registry itself is a shared, highly-read service, and its load is driven by one arithmetic fact: registry QPS ≈ (number of clients) × (1 / refresh interval). With 2,000 caller instances each polling every 30s, that's already 2000 / 30 ≈ 67 QPS of steady-state reads — fine on its own, but it is the steady state that is not the danger.

Two designs answer "how does a client learn the list changed":

The real herd shows up at the edges, not in steady state: a mass client restart (a rolling deploy, a datacenter failover, every pod restarting after a bad config push) means thousands of clients re-registering and re-fetching in the same few seconds — the registry sees a burst many multiples of its steady-state QPS, right when it's least equipped to absorb it (the same event probably also stressed the network). A watch-based registry has the mirror problem: one change (say, a registry node itself flapping) can fire every open watch simultaneously, producing the identical spike from the other direction.

The fix in both directions is the same: never let every client wake up on the same clock. Add jitter (randomize each client's refresh/reconnect interval by, say, ±20–30%) so a synchronized restart de-synchronizes within a couple of cycles, and use exponential backoff on reconnect/re-register failures so a struggling registry isn't hit harder by the very clients whose requests are already failing. This is the identical damping logic used for retries against any overloaded dependency — the registry is not special, it's just usually the first thing to feel a mass-restart storm because every single instance depends on it to start serving traffic at all.

4. Circuit-breaker state is per-instance — by default, N breakers, not one

A circuit breaker's Closed/Open/Half-Open state normally lives in the process memory of the caller that created it. If orders runs 50 instances and each one calls payments, that is 50 separate breakers, each keeping its own failure count, each deciding independently when to trip and when to try recovery. Nothing shares that state unless you explicitly build a shared store for it.

Traced scenario. payments goes down; all 50 orders breakers see failures and trip to Open within seconds of each other — that part is fine, they're reacting to the same real signal. payments recovers at t=0. Every one of those 50 breakers is running its own independent Open-duration timer (say, 30s, started at slightly different moments as each one tripped). As each timer expires, that instance moves to Half-Open and sends its one trial probe — with no idea any other caller is doing the same thing. If enough of those 30s timers happen to expire close together (likely, since they were all started within the same few-second window when the outage began), dozens of half-open probes can land on the freshly-recovered payments at nearly the same instant. A service that had just enough capacity to come back up now takes a synchronized hit from its entire caller fleet — and can relapse under the exact probes meant to confirm it was safe to reopen.

Two structural fixes, and they compose:

The trade-off is direct: per-instance is free, needs no extra infrastructure, and adds zero latency to the fast path — but you accept N uncoordinated views of reality. Coordinated is consistent — but the shared store is now on your hot path, it must be fast and highly available itself, and it must fail open to the per-instance behavior if it becomes unreachable, or you've built a new single point of failure to protect against the old one.

Two lines worth remembering cold: "trip sooner, not probe sooner" — lowering your error-rate trip threshold so the breaker opens before the dependency is fully saturated does more good than tightening the half-open retry cadence, which just adds more probes to a system already struggling. And: "state is per-instance" — unless you deliberately built a shared store, one caller's breaker being Open tells you nothing about what the other 49 are doing right now.

Per-instance circuit breakers each independently probe a recovering service versus a coordinated shared breaker that gates a single permit
Per-instance circuit breakers each independently probe a recovering service versus a coordinated shared breaker that gates a single permit

5. Spike-tolerant thresholds, and why a slow dependency is worse than a dead one

A breaker that trips on a raw error-rate percentage over a window (say, "open if ≥50% of the last 20 calls failed") is deliberately tolerant of a brief spike — one bad deploy blip or a two-second GC pause shouldn't trip the whole circuit. But that same tolerance is dangerous under sustained overload: if a dependency is merely slow rather than erroring outright, the error-rate signal can sit comfortably under the trip threshold (calls are timing out or queuing, not returning HTTP 500) while the damage compounds elsewhere. This is why production breakers combine an error-rate trip with a separate, usually lower, slow-call-rate threshold (resilience4j's slowCallRateThreshold) — sustained degraded latency needs to shed load even when outright failures never cross the error-rate bar.

The reason slow beats dead as a failure mode, precisely, is Little's Law: L = λ × W — the number of requests in flight (L) equals the arrival rate (λ) times the average time each one spends in the system (W).

Scenarioλ (arrivals/s)W (time in system)L = λ×W (in-flight)
Normal500 req/s50 ms25 in-flight
Dependency goes slow (not down)500 req/s (still arriving)4,000 ms2,000 in-flight
Dependency fails fast (breaker open)500 req/s5 ms (immediate reject)2.5 in-flight

With a 200-connection/thread pool sized for the normal case (25 in-flight, 8× headroom), the slow scenario fills all 200 slots in 200 ÷ 500 ≈ 0.4 seconds — and every request after that either queues or is rejected, including requests to unrelated endpoints if the pool is shared. The fast-failure scenario keeps L at 2.5, nowhere near the pool ceiling; the caller absorbs the error immediately and moves on. Same arrival rate, wildly different blast radius — because W, not the failure itself, is what decides how many resources the failure holds hostage. This is the mechanical reason behind the maxim "slow is worse than down", and it's the same argument for why a circuit breaker should trip on latency degradation, not only on hard errors — the pool exhaustion in row two happens whether or not a single request ever technically "failed."

Pitfalls

Selection & trade-offs: the judgment layer

AP vs CP registry. Default to AP (Eureka-style) for plain service discovery: the only question being answered is "give me a good-enough address," a stale entry costs one retry, and a registry that goes silent during a partition is strictly worse than one that answers with slightly old data. Reach for CP (Consul/etcd/ZooKeeper with quorum reads) when the same store is also arbitrating something where two different answers is a correctness bug, not an inconvenience — leader election, distributed locks, config that must be seen identically everywhere. Don't pay the CP tax (discovery going dark on a minority partition) for a job that only ever needed "a" healthy instance, not "the" consistent view.

Per-instance vs coordinated circuit breaker. Default to per-instance: zero extra infrastructure, zero added latency, and with a half-open concurrency guard the blast radius of uncoordinated probes is usually tolerable. Move to a coordinated/shared breaker when the caller fleet is large enough that N simultaneous half-open probes reliably re-trip a fragile dependency, or when policy genuinely needs to be global (one signal every caller must honor, e.g. protecting a shared internal gateway that has no headroom for even a handful of simultaneous probes). The cost you're accepting is a new dependency in the hot path that must be fast, highly available, and must fail open — never let the safety mechanism become the outage.

Takeaways

Related pages


Sources: Netflix Eureka wiki and Spring Cloud Netflix Eureka docs (renewal/lease/eviction defaults and self-preservation mode); HashiCorp Consul and Apache ZooKeeper/etcd documentation (quorum-based consistency); resilience4j CircuitBreaker docs (half-open permitted calls, slow-call-rate threshold); Michael Nygard, Release It! (2nd ed., 2018) — the circuit breaker stability pattern; John D. C. Little, "A Proof for the Queuing Formula: L = λW" (1961); Google SRE Book, ch. on cascading failures and load shedding; the AWS Builders' Library, "Timeouts, retries, and backoff with jitter." Re-authored/Deepened for this guide.

🤖 Don't fully get this? Learn it with Claude

Stuck on Service Discovery & Distributed Circuit Breakers — Detection Lag, CAP Stance, Registry Herd & Per-Instance vs Coordinated State (Deep Dive)? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Service Discovery & Distributed Circuit Breakers — Detection Lag, CAP Stance, Registry Herd & Per-Instance vs Coordinated State (Deep Dive)** (System Design) and want to truly understand it. Explain Service Discovery & Distributed Circuit Breakers — Detection Lag, CAP Stance, Registry Herd & Per-Instance vs Coordinated State (Deep Dive) from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Service Discovery & Distributed Circuit Breakers — Detection Lag, CAP Stance, Registry Herd & Per-Instance vs Coordinated State (Deep Dive)** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Service Discovery & Distributed Circuit Breakers — Detection Lag, CAP Stance, Registry Herd & Per-Instance vs Coordinated State (Deep Dive)** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Service Discovery & Distributed Circuit Breakers — Detection Lag, CAP Stance, Registry Herd & Per-Instance vs Coordinated State (Deep Dive)** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes