When Envoy returns a 503 with body “no healthy upstream” and access log flag UH, the cluster has zero hosts available for load balancing. The cluster exists, routes point to it, traffic is flowing, but the load balancer cannot select a host.
The response body is literal. The flag UH appears in the %RESPONSE_FLAGS% access log field and means “No healthy upstream hosts in upstream cluster in addition to 503 response code.” It is distinct from UO (circuit breaker overflow), UF (upstream connection failure), and NR (no route).
The cause is rarely a bug in Envoy itself. It is one of: a genuine upstream outage, a health-check misconfiguration that fails healthy hosts, an EDS scale-down that removed all endpoints, or an outlier-detection cascade that ejected every host. The diagnostic path and the fix differ for each.
The critical subtlety is the panic threshold. The default is 50%. When the healthy host percentage in a priority drops below this, Envoy load-balances across ALL hosts including unhealthy ones rather than overloading the few that remain. The observed error rate can understate the damage: Envoy is deliberately sending traffic to hosts it knows are bad. A cluster that looks 10% unhealthy can be 90% unhealthy with panic mode masking the true severity.
What this means
Envoy tracks cluster membership in health states: healthy, degraded, and excluded from load balancing. The load balancer selects from healthy hosts. When membership_healthy reaches zero while membership_total is greater than zero, every routed request gets a local 503 with body “no healthy upstream” and flag UH.
The panic threshold changes this behavior. With the default 50% threshold:
- If 50% or more of hosts are healthy: traffic goes to healthy hosts only.
- If fewer than 50% are healthy: panic mode engages, and traffic goes to ALL hosts regardless of health status.
- If
membership_healthy == 0ANDmembership_total > 0: no host qualifies even in panic mode, and you getUH.
The lb_healthy_panic counter increments when panic mode is active. If you see UH errors, panic mode has already failed to help because every host is unhealthy. If you see elevated error rates without UH, check whether lb_healthy_panic is incrementing: Envoy may be routing to known-bad hosts, and the error rate understates the damage.
The error rate is a lagging and understated signal below the panic threshold. Alerting on UH alone catches the cliff, not the slide toward it.
flowchart TD
A["503 UH on cluster"] --> B{"membership_total > 0?"}
B -- No --> C["EDS scale-down
DNS failure
control plane disconnect"]
B -- Yes --> D{"ejections_active
high vs total?"}
D -- Yes --> E["Outlier-detection
cascade"]
D -- No --> F{"connect_fail
increasing?"}
F -- Yes --> G["Genuine outage
or network partition"]
F -- No --> H{"Correlates with
config push?"}
H -- Yes --> I["CDS update
transient"]
H -- No --> J["Health-check
misconfiguration"]Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Genuine upstream outage | membership_healthy drops to 0, connect_fail increasing, hosts not accepting connections | Whether upstream processes are running and listening |
| Health-check misconfiguration | membership_healthy drops but hosts are serving traffic; health endpoint returns non-200 or wrong path | Health check config: path, port, expected response |
| EDS scale-down | membership_total drops to 0 or near 0, membership_healthy follows | Control plane or service registry; was a deploy or scale event intended? |
| Outlier-detection cascade | ejections_active high and climbing, ejections_overflow incrementing, hosts pass health checks but are ejected | outlier_detection.ejections_active and the per-type ejection counters |
| CDS update transient | Brief membership_healthy == 0 window during a cluster config push, resolves in seconds | Whether failure correlates with a CDS update |
| STRICT_DNS resolution failure | update_failure increasing on DNS-based clusters, hosts never resolve at startup | DNS server reachability and resolver timeout |
| TLS/SDS health-check failure | Health checks fail after restart or cert rotation, ssl.connection_error on the cluster | Whether SDS secrets were delivered before cluster creation |
Quick checks
# Check membership state for the affected cluster
curl -s http://localhost:9901/stats | grep 'cluster.<name>.membership'
# Check for panic mode activity
curl -s http://localhost:9901/stats | grep 'lb_healthy_panic'
# Check upstream connection failures (genuine outage indicator)
curl -s http://localhost:9901/stats | grep 'cluster.<name>.upstream_cx_connect_fail'
# Check outlier detection ejections
curl -s http://localhost:9901/stats | grep 'cluster.<name>.outlier_detection'
# Check EDS update health
curl -s http://localhost:9901/stats | grep -E 'cluster.<name>.(update_success|update_failure|update_empty|update_rejected)'
# Per-host health detail from the cluster
curl -s http://localhost:9901/clusters?format=json | jq '.cluster_statuses[] | select(.name == "<name>") | .host_statuses[] | {address: .address, health_status: .health_status}'
# Check control plane connectivity
curl -s http://localhost:9901/stats | grep 'control_plane.connected_state'
# Verify Envoy is LIVE (not stuck initializing)
curl -s -o /dev/null -w "%{http_code}" http://localhost:9901/ready
In Istio sidecar mode, use port 15000 for the Envoy admin interface. The readiness check is on port 15021 (/healthz/ready).
How to diagnose it
Confirm the cluster has traffic flowing. A cluster with
membership_healthy == 0but zero routed traffic is not user-impacting. Checkcluster.<name>.upstream_rq_totalis non-zero. If traffic is not flowing, the unhealthy cluster is inert.Check
membership_total. This splits the diagnosis in half. Ifmembership_total > 0, hosts are known but unhealthy: the problem is health state, not discovery. Ifmembership_total == 0, Envoy has no hosts at all: the problem is discovery (EDS, DNS, or control plane).If
membership_totaldropped, check EDS and the control plane. Look atupdate_success,update_failure,update_empty, andupdate_rejectedfor the cluster. A spike inupdate_emptymeans the control plane sent an update with no endpoints. Checkcontrol_plane.connected_stateis 1. If the control plane is disconnected, Envoy is running stale config and the scale-down never reached it, or endpoints were removed upstream but Envoy still holds old ones.If
membership_totalis stable butmembership_healthyis 0, check outlier detection. Pulloutlier_detection.ejections_activeandejections_overflow. Ifejections_activeis high relative tomembership_total, hosts are being ejected by passive health detection. Look at the per-type counters (ejections_enforced_consecutive_5xx,ejections_enforced_success_rate,ejections_enforced_failure_percentage) to identify the trigger.If ejections are low or zero, check active health checks. Look at the health check failure stats for the cluster. If
health_check.failureis increasing but the upstream is actually serving traffic, the health check is misconfigured. Verify the path, port, expected status, and timeout against the actual upstream behavior by hitting the endpoint directly from the Envoy host.If health checks are passing but
membership_healthyis still 0, check for a cold start or CDS transient. On startup, Envoy excludes hosts from load-balancing calculations until their first health check completes. A cluster that just received a CDS update can briefly showmembership_healthy == 0even with healthy backends. If the failure resolves within seconds and correlates with a config push, this is the likely cause.Distinguish genuine outage from network partition. If
upstream_cx_connect_failis increasing alongside the health check failures, hosts are not accepting connections. This is either a real outage or a network or ACL problem between Envoy and the upstream. Test connectivity directly from the Envoy host to one of the upstream endpoints using the same port.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
cluster.<name>.membership_healthy | Hosts available for load balancing | Reaching 0 with traffic flowing |
cluster.<name>.membership_total | Whether hosts are known at all | Dropping to 0 indicates discovery failure |
cluster.<name>.lb_healthy_panic | Panic mode is active, traffic going to unhealthy hosts | Any increment means the cluster is in worse shape than the error rate suggests |
cluster.<name>.outlier_detection.ejections_active | Hosts ejected by passive detection | Approaching membership_total signals a cascade |
cluster.<name>.outlier_detection.ejections_overflow | Ejection cap reached, cluster worse than detection can address | Any increment |
cluster.<name>.update_empty | Control plane sent zero endpoints | Sustained on a cluster that should have endpoints |
cluster.<name>.upstream_cx_connect_fail | Hosts not accepting connections | Sustained nonzero rate |
%RESPONSE_FLAGS% with UH | Confirms no-healthy-upstream origin of the 503 | Any nonzero rate on production traffic |
control_plane.connected_state | Whether Envoy has current config | 0 sustained for more than 5 minutes |
Fixes
Genuine upstream outage
The fix is upstream, not Envoy. Verify the upstream processes are running, listening on the expected port, and not resource-exhausted. If connect_fail is high, the hosts are not accepting connections. Check FD exhaustion, SYN backlog, and process health on the upstream side. Envoy is correctly reporting the failure.
If the outage is partial, the panic threshold will keep traffic flowing to the remaining hosts plus the unhealthy ones. Do not disable the panic threshold during an active outage: that forces Envoy to return UH immediately instead of attempting degraded service.
Health-check misconfiguration
If hosts are serving traffic but Envoy reports them unhealthy, the health check is wrong. Common causes: wrong path (health check hits an endpoint that returns 404), wrong port, expected response code mismatch (check expects 200, endpoint returns 204), or timeout too short for the upstream’s health response.
Verify by hitting the health check endpoint directly from the Envoy host using the same path, port, and headers Envoy uses. The /clusters admin endpoint shows the configured health check and per-host health status. Fix the configuration via xDS or static config and push the update.
EDS scale-down
If membership_total dropped, the control plane removed endpoints. This may be intended (a deploy, a scale-down) or unintended (a bug in the service registry, a label selector change). Check the control plane side. In Istio, verify the ServiceEntry and DestinationRule. Check update_empty to confirm the control plane sent an empty endpoint set. If unintended, fix the control plane configuration. If intended, the cluster should recover as new endpoints register.
Outlier-detection cascade
If ejections_active is high and ejections_overflow is incrementing, outlier detection is ejecting hosts faster than they recover. The root cause is usually a shared dependency failure (all hosts slow because of a database) or overly sensitive thresholds.
Short-term: the panic threshold is already doing its job by routing to ejected hosts. Do not fight it. Long-term: tune the outlier detection thresholds (consecutive_5xx, success_rate_request_volume, failure_percentage_threshold) and verify max_ejection_percent is appropriate for your cluster size.
CDS update transient
Brief UH errors during a CDS update are a known issue. The new cluster’s load balancer is used before its host set is initialized. This resolves in seconds and does not indicate a persistent problem. If the errors are sustained, look for a different cause.
STRICT_DNS resolution failure
For DNS-based clusters, if update_failure is increasing and hosts never resolve, DNS is the bottleneck. This is especially common at startup with many STRICT_DNS clusters. Envoy does not retry DNS aggressively enough on initial resolution. Workarounds include increasing the DNS resolver timeout or ensuring DNS is reachable before Envoy starts.
TLS/SDS health-check failure
If health checks use upstream TLS with a client certificate delivered via SDS, health checks fail when the SDS secret is not ready at cluster creation. This causes a window of 503s after restart proportional to healthy_threshold * no_traffic_interval. The fix is ordering: ensure SDS secrets are delivered before or simultaneously with CDS/EDS resources.
Prevention
- Alert on
membership_healthy / membership_totalratio, not justUH. Catch the slide before the cliff. Page when the ratio drops below 50% with traffic flowing and the cluster has been live for more than 600 seconds. - Monitor
lb_healthy_panic. Any increment means the cluster is in worse shape than the error rate suggests, because traffic is going to known-unhealthy hosts. - Track both
ejections_activeandejections_overflow. Outlier-detection cascades are preventable with proper thresholds.ejections_overflowmeans the cluster is worse than detection can address. - Alert on
control_plane.connected_state == 0sustained. Stale config is a delayed-action failure. Also trackupdate_rejected: Envoy can be connected but silently NACKing every update. - Track
update_emptyandupdate_rejected. Silent discovery failures and rejected configs are the most missed signals in Envoy operations. - Test health check endpoints independently. A health check that does not match actual upstream behavior will fail healthy hosts under load. Verify path, port, expected status, and timeout against the real endpoint.
- Know your Envoy version. Behavior around health-check initialization and panic calculations has changed across versions. Hosts may be excluded from load-balancing calculations until their first health check completes.
How Netdata helps
Netdata collects Envoy admin stats at per-second resolution, which matters when membership changes happen in seconds:
membership_healthyandmembership_totalper cluster at per-second granularity show whethermembership_totalfell (discovery) ormembership_healthyfell (health state), and exactly when.outlier_detection.ejections_activeandejections_overflowon the same timeline as membership drops separate passive-detection cascades from active health-check failures.upstream_cx_connect_failalongside membership changes distinguishes connection-refused (real outage) from health-check misconfiguration.control_plane.connected_stateand clusterupdate_*counters reveal whether an EDS scale-down or disconnect preceded the failure.- ML anomaly detection on the
membership_healthy / membership_totalratio can flag gradual declines beforeUHtriggers.
Related guides
- Envoy membership_healthy dropping: reading the single most important cluster signal
- Envoy outlier detection mass ejection: when passive health checks empty a cluster
- Envoy panic threshold: why traffic routes to unhealthy hosts at 50%
- Envoy upstream_cx_connect_fail: failed TCP connections to upstream hosts
- Envoy health checks vs outlier detection: two systems that eject hosts differently
- Envoy monitoring checklist: the signals every production proxy needs
- How Envoy actually works in production: a mental model for operators
- Envoy monitoring maturity model: from survival to expert
- Envoy upstream_rq_pending_overflow: the pending queue fills and 503s begin
- Envoy circuit breaker open: cx_open, rq_pending_open, and fast-failed requests
- Envoy upstream_cx_active near max_connections: the pool filling up
- Envoy connection pool exhaustion: a slow upstream that fills the pool






