Services routing to nothing
What does ZopNight detect here?
A Service whose EndpointSlice is empty has no pods behind it, so every request to it fails. ZopNight reads the EndpointSlice rather than the Service, because a selector can look correct and still match 0 pods, which is the failure this catches.
Signal and threshold
| Field | Value |
|---|---|
| Rule IDs | RC-1742 · RC-1842 · RC-1942 |
| Category | orphan |
| Severity | medium |
| Metric | none — pure configuration read |
| Source | internal/rules/k8s/endpoint_empty.go |
Where it applies
The Service looks fine; the EndpointSlice is empty
A Kubernetes Service is a selector plus a stable address. It does not verify that anything matches. Apply a Service with a typo in one label and Kubernetes accepts it happily: the object is valid, the ClusterIP is allocated, DNS resolves.
What is missing is the EndpointSlice: the list of pod IPs the Service actually routes to. Reading the Service tells you what it intends to match. Reading the EndpointSlice tells you what it found, which is why this rule looks there.
What callers experience
Connection refused, immediately, on a hostname that resolves. Not a timeout, not a 503 from a proxy. The failure is at the TCP level, and it looks like a network problem rather than a configuration one.
Teams typically debug DNS first, because the name resolves and the address exists, and only later discover the Service was matching nothing all along.
The usual causes
Label drift. The Deployment’s pod template labels changed and the Service selector did not. Both objects are individually valid.
Namespace mismatch. A Service only selects pods in its own namespace. Copying a manifest between namespaces breaks the pairing silently.
Zero ready pods. The selector is correct but every pod is failing readiness, so none are added to the EndpointSlice. Here the Service is not the problem. It is reporting one accurately.
That third case is worth separating before changing anything: an empty EndpointSlice may be a symptom rather than a misconfiguration.
Empty EndpointSlices, then selector versus labels
kubectl get endpointslices -A -o json | jq -r ' .items[] | select((.endpoints // []) | length == 0) | "\(.metadata.namespace)/\(.metadata.labels["kubernetes.io/service-name"])"'Then compare the selector against the actual pod labels:
kubectl get svc <name> -n <ns> -o jsonpath='{.spec.selector}'kubectl get pods -n <ns> --show-labelsThe cost consequence
A ClusterIP Service costs nothing, so an empty one is free. A LoadBalancer Service is not.
It provisions a real cloud load balancer that bills continuously whether or not any pod sits
behind it. Empty LoadBalancer Services are the ones worth finding first.