Troubleshooting a Service With No Endpoints
Two broken Services with identical symptoms. One has an empty EndpointSlice from a one-letter typo; the other has two healthy endpoints and still refuses every connection. Checking the slice first is what separates them.
Troubleshooting Guide 98 of 103 Intermediate
- Kubernetes1.36.4
- Cluster4 nodes
- Runtimecontainerd 2.2.6
- CNICalico v3.32.1
- TimeAbout 25 min
- Reviewed21 August 2026
Written against the versions above. The curl exit codes are the fastest signal here and are worth learning: 7 is refused, 28 is a timeout.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA1001 | 192.168.0.175 | Ubuntu 26.04 LTS | Control Plane Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE01 | 192.168.0.176 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE02 | 192.168.0.177 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE03 | 192.168.0.178 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
Before you start
- The Services guide and the EndpointSlices guide.
- A shell Pod with
curl.nicolaka/netshootis used here. - A scratch namespace.
-
A one-letter typo, and an empty slice
The Service selects
app: webb. The Deployment's Pods are labelledapp: web. One letter.The Service is created without complaint, gets a ClusterIP, and looks entirely normal in
kubectl get svc. Nothing anywhere reports a problem, because nothing is wrong with the object: a selector that matches nothing is a valid selector.The EndpointSlice is where it shows:
web-typo-j8ncx endpoints=nullAnd
curlfails with exit code 7, connection refused. Not a timeout: DNS resolved, the ClusterIP was reached, and there was no rule to rewrite the packet to, so it was refused immediately.The two-command diagnosis, and this is the whole method:
kubectl get svc <name> -o jsonpath='{.spec.selector}' kubectl get pods -l <that selector>selector={"app":"webb"}thenNo resources found. Conclusive, in under ten seconds, with no guessing.kubectl describe svcshows the same thing more compactly:Selector: app=webbabove an emptyEndpoints:line. Reading those two lines together is the habit worth building.The usual causes, in rough order of frequency: a typo; labels placed on the Deployment's
metadatarather than itsspec.template.metadataso the Pods never get them; and a Service in a different namespace from its Pods, since a selector never crosses namespaces.bash Example session kubectl get pods -n t3 --show-labels --no-headers | awk '{print $1, $6}'client <none>web-7fbc579fd4-rxn8q app=web,pod-template-hash=7fbc579fd4web-7fbc579fd4-xr86g app=web,pod-template-hash=7fbc579fd4kubectl apply -f - <<'EOF'apiVersion: v1kind: Servicemetadata: name: web-typo namespace: t3spec: selector: app: webb ports: - {port: 80, targetPort: 80}EOFservice/web-typo createdsleep 10; kubectl get svc web-typo -n t3NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEweb-typo ClusterIP 10.97.101.148 <none> 80/TCP 10skubectl get endpointslice -n t3 -l kubernetes.io/service-name=web-typo -o jsonpath="{range .items[*]}{.metadata.name}{\" endpoints=\"}{.endpoints}{\"\n\"}{end}"web-typo-j8ncx endpoints=nullkubectl exec client -n t3 -- curl -s -o /dev/null -w '%{http_code} exit=' --max-time 5 http://web-typo; echo $?000 exit=command terminated with exit code 77kubectl get svc web-typo -n t3 -o jsonpath="selector={.spec.selector}{\"\n\"}"selector={"app":"webb"}kubectl get pods -n t3 -l app=webb --no-headers 2>&1 | tail -1No resources found in t3 namespace.kubectl describe svc web-typo -n t3 | grep -E 'Selector|Endpoints'Selector: app=webbEndpoints:Expected result
endpoints=null, exit 7, and the selector matching nothing. Notepod-template-hashin the labels: that is added by the ReplicaSet, and selecting on it works but breaks on the next rollout.Success condition
kubectl get pods -l <selector>returns nothing for the Service's own selector. -
Endpoints present, connection still refused
Now the case that catches people who learned the previous step and stopped there. This Service's selector is correct,
app: web, and the EndpointSlice proves it:["10.244.93.63"] ["10.244.100.44"] port=8080Two healthy endpoints. And
curlstill fails with exit 7, identical to the typo case.The difference is in that same output:
port=8080. The Service'stargetPortis 8080, and nginx listens on 80. The endpoints are correct, kube-proxy's rules are correct, and the packet is being delivered to a port where nothing is listening. The container refuses it, which is why the failure is a refusal rather than a timeout.So the two cases are told apart by the slice, not by the symptom:
- Empty slice -> the selector is wrong.
- Populated slice, still refused -> the port is wrong, or the container is not listening on the port it should be.
The last command is worth its own note.
.spec.containers[0].portsreturns empty, because this Deployment was created withkubectl create deploymentand declares nocontainerPortat all.That is not the bug.
containerPortis documentation: it does not open, publish or restrict anything, and a Service'stargetPortcan name a port the container never declared. It works if the process is listening and fails if it is not, regardless of what the spec says.Which means the spec cannot answer the question. To find what a container is really listening on, ask it:
kubectl exec, or test the Pod IP and port directly from another Pod.-- netstat -tlnp bash Example session kubectl apply -f - <<'EOF'apiVersion: v1kind: Servicemetadata: name: web-badport namespace: t3spec: selector: app: web ports: - {port: 80, targetPort: 8080}EOFservice/web-badport createdsleep 10; kubectl get endpointslice -n t3 -l kubernetes.io/service-name=web-badport -o jsonpath="{range .items[*]}{.endpoints[*].addresses}{\" port=\"}{.ports[*].port}{\"\n\"}{end}"["10.244.93.63"] ["10.244.100.44"] port=8080kubectl exec client -n t3 -- curl -s -o /dev/null -w '%{http_code} exit=' --max-time 5 http://web-badport; echo $?000 exit=command terminated with exit code 77kubectl get svc web-badport -n t3 -o jsonpath="port={.spec.ports[0].port}{\" targetPort=\"}{.spec.ports[0].targetPort}{\"\n\"}"port=80 targetPort=8080kubectl get pod -n t3 -l app=web -o jsonpath="{.items[0].spec.containers[0].ports}{\"\n\"}"Expected resultTwo endpoints and a refused connection. The empty
portsoutput is real: the container declares none, which changes nothing about what it listens on.Success conditionThe slice is populated and the connection still fails.
-
The same Service done right
kubectl expose --target-port=80produces the working version, and it is worth seeing the healthy output so the broken ones have something to be compared against.Two endpoints, both
ready=true, and a 200.So the full diagnostic order for a Service that will not serve, in the order that eliminates the most per command:
kubectl get endpointslice -l kubernetes.io/service-name=- empty means the selector, populated means look further.- Read the port from the slice - that is what traffic is being sent to, and comparing it against what the container listens on settles the second case.
- Test the Pod IP directly -
curlfrom another Pod. Working here and failing through the Service points at kube-proxy; failing both ways points at the container.: - Check readiness - endpoints marked
ready=falseare excluded from load balancing, so a Service with only unready endpoints behaves exactly like one with none.
And the exit codes, which are the cheapest signal available:
- 7 - connection refused. No endpoint, wrong port, or nothing listening.
- 28 - timeout. Packets are being dropped: NetworkPolicy, a firewall, or a broken overlay.
- 6 - could not resolve the name. A DNS problem, not a Service problem.
That last distinction matters more than it looks. Exit 6 or a DNS timeout means the Service may be perfectly healthy and the client simply cannot find it, which is a different guide.
bash Example session kubectl expose deployment web -n t3 --name=web-ok --port=80 --target-port=80service/web-ok exposedsleep 10; kubectl get endpointslice -n t3 -l kubernetes.io/service-name=web-ok -o jsonpath="{range .items[*].endpoints[*]}{.addresses}{\" ready=\"}{.conditions.ready}{\"\n\"}{end}"["10.244.93.63"] ready=true["10.244.100.44"] ready=truekubectl exec client -n t3 -- curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 http://web-ok200Expected resultThe same two Pod IPs as the broken Service had, now with the right port, returning 200. The endpoints were never the problem in step 2.
Success condition
curlthrough the corrected Service returns 200.
Troubleshooting
A Service has no endpoints and the labels look right.
Why: Usually labels on the Deployment rather than in its Pod template.
metadata.labelson a Deployment is not inherited by its Pods.Fix:Compare what the Pods actually carry:
kubectl get pods --show-labels. Onlyspec.template.metadata.labelsreaches the Pods. Then check the Service's selector against that withkubectl get pods -l <selector>; no output is conclusive.Endpoints exist but every connection is refused.
Why:
targetPortdoes not match what the container listens on, or the process binds only to 127.0.0.1.Fix:Read the port from the slice, then ask the container:
kubectl exec <pod> -- netstat -tlnporss -tlnp. A process bound to127.0.0.1:80rather than0.0.0.0:80accepts nothing from outside the container and produces exactly this symptom while looking correct in every manifest.The Service works intermittently.
Why: Some endpoints are not ready, or some Pods are broken while others serve. kube-proxy balances across ready endpoints only, so a partially broken set produces failures on a fraction of requests.
Fix:
kubectl get endpointslice -l kubernetes.io/service-name=<svc> -o jsonpath='{range .items[*].endpoints[*]}{.addresses} ready={.conditions.ready}{"\n"}{end}'. Then test each Pod IP directly to find which ones fail. A readiness probe that actually checks the application is what prevents this.targetPortnames a port the container never declared and you are unsure whether that is the bug.Why: It is not.
containerPortis informational and does not open or restrict anything.Fix:Ignore the absence of
containerPortentirely; it changes nothing at runtime. What matters is the port the process binds. DeclaringcontainerPortis still worth doing as documentation, and it lets a Service use a namedtargetPort, which is more robust than a number when the port changes.The Service resolves and connections time out rather than being refused.
Why: A timeout means packets are dropped, not rejected. That is a different class of fault from a missing endpoint.
Fix:curl exit 28 rather than 7. Look at NetworkPolicy first (
kubectl get netpol -n <ns>), then the CNI. The NetworkPolicy guides cover the diagnosis; an empty-endpoint Service refuses immediately and never times out.A headless Service appears to have no endpoints.
Why: It has no ClusterIP by design, so
kubectl get svcshowsNoneand there are no kube-proxy rules. That is not the same as having no endpoints.Fix:Check the slice as usual, then resolve the name: a headless Service returns one A record per ready Pod. If DNS returns records, it is working.
nslookup <svc>.<ns>.svc.cluster.localfrom a Pod is the real test, not a ClusterIP that was never meant to exist.