Troubleshoot a Kubernetes Service That Is Not Working

Troubleshoot a Kubernetes Service that does not route traffic by testing Pod IP, EndpointSlices, ClusterIP, DNS, and external exposure in an inside-out workflow.

Published

Updated

Read time 14 min read

Reviewed byDeepak Prasad

Troubleshoot Kubernetes Service routing with EndpointSlice selector port and readiness checks
Tested on Rocky Linux 10.2 (Red Quartz) workstation
Package kubectl 1.36.3
Applies to Bash-compatible Linux or macOS shell with kubectl configured; Kubernetes 1.27+ cluster with Linux worker nodes
Cert prep CKAD · CKA
Lab environment Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm
Privilege Normal user (no sudo required on the workstation)
Scope Inside-out Service troubleshooting from Pod IP through EndpointSlices, ClusterIP, DNS, and external exposure; selector and readiness checks; port and targetPort mapping; direct Pod tests; listener address; NetworkPolicy pointers; NodePort and LoadBalancer last-step checks; and a decision flow. Does not cover Service creation tutorials, full CoreDNS runbooks, kube-proxy internals, CNI repair, provider LoadBalancer administration, or deep Ingress-controller debugging.
IMPORTANT
This guide troubleshoots Service routing when a Service object exists but clients cannot connect through its DNS name, ClusterIP, NodePort, or external address. It does not teach Service YAML from scratch or replace Kubernetes DNS troubleshooting when only name resolution fails.

You can see the Service in kubectl get and still get connection refused, timeouts, or empty replies. Work from the application Pod outward: confirm the process listens on the right port, test Pod IP directly, verify EndpointSlices, then ClusterIP, DNS, and external exposure.

text
Application process → Pod IP → EndpointSlice → ClusterIP → DNS → external exposure

Quick reference

Test from a client Pod inside the cluster before you curl NodePort or LoadBalancer from your workstation. Replace NS, SVC, POD, and PORT with your values.

Step Command What to check
1 kubectl get pods -n NS -l app=APP -o wide Backend Pod IP and Ready state
2 kubectl exec -n NS CLIENT -- curl -sS --max-time 3 POD_IP:TARGET_PORT Application listens on the Service targetPort
3 kubectl get endpointslice -n NS -l kubernetes.io/service-name=SVC -o wide Empty addresses → selector mismatch; ready=false → probes or termination
4 kubectl exec -n NS CLIENT -- curl -sS --max-time 3 SVC_CLUSTER_IP:SVC_PORT Service port mapping when Pod IP already works
5 kubectl exec -n NS CLIENT -- getent hosts SVC.NS.svc.cluster.local DNS when ClusterIP works — see DNS troubleshooting
6 NodePort or LoadBalancer last External exposure only after in-cluster routing succeeds — see decision flow

Prepare a Reproducible Service Lab

Deploy the backend and Service

The walkthrough uses a two-replica nginx web Deployment and ClusterIP Service in svc-trouble-lab. Apply this manifest before the client Pod:

yaml
apiVersion: v1
kind: Namespace
metadata:
  name: svc-trouble-lab
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: svc-trouble-lab
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: app
        image: nginx:1.27-alpine
        command: ["/bin/sh", "-c"]
        args:
        - |
          echo "server $(hostname)" > /usr/share/nginx/html/index.html
          touch /tmp/healthy
          exec nginx -g "daemon off;"
        ports:
        - name: http
          containerPort: 80
        readinessProbe:
          exec:
            command: ["/bin/sh", "-c", "test -f /tmp/healthy"]
          initialDelaySeconds: 2
          periodSeconds: 3
---
apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: svc-trouble-lab
spec:
  selector:
    app: web
  ports:
  - name: http
    port: 80
    targetPort: http
bash
kubectl apply -f web-lab.yaml
bash
kubectl wait -n svc-trouble-lab --for=condition=Available deployment/web --timeout=180s

For Service types and EndpointSlice basics, see Kubernetes Services.

Create the client Pod

Testing only from your workstation cannot separate ClusterIP routing problems from NodePort, LoadBalancer, or Ingress issues. Run network checks from a Pod inside the cluster first. For how Services, CNI, and kube-proxy relate before you test routing, see Kubernetes networking.

This workflow uses a lightweight client image with curl. The curl command covers -w, --max-time, and exit codes for quick HTTP checks from Pods. Use the dedicated DNS troubleshooting guide when you need nslookup or dig. Add this Pod to the same namespace:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: net-client
  namespace: svc-trouble-lab
spec:
  containers:
  - name: tools
    image: curlimages/curl:8.5.0
    command: ["sleep", "infinity"]

Apply the client Pod:

bash
kubectl apply -f net-client.yaml
bash
kubectl wait -n svc-trouble-lab --for=condition=Ready pod/net-client --timeout=120s

Keep net-client running so every test uses the same network path.

Capture Service, Pod, and EndpointSlice values

Before you change anything, capture the current Service, Pod, and EndpointSlice state.

List the Service with type and ClusterIP:

bash
kubectl get service web -n svc-trouble-lab -o wide

Read ports, selector, and legacy Endpoints summary:

bash
kubectl describe service web -n svc-trouble-lab

List backend Pods with labels and IPs:

bash
kubectl get pods -n svc-trouble-lab -o wide --show-labels

Inspect EndpointSlices for the Service name:

bash
kubectl get endpointslice -n svc-trouble-lab -l kubernetes.io/service-name=web

After the Deployment is ready, capture live values for later tests:

bash
SERVICE_IP=$(kubectl get service web -n svc-trouble-lab -o jsonpath='{.spec.clusterIP}')
bash
BACKEND_POD=$(kubectl get pods -n svc-trouble-lab -l app=web -o jsonpath='{.items[0].metadata.name}')
bash
BACKEND_IP=$(kubectl get pod "$BACKEND_POD" -n svc-trouble-lab -o jsonpath='{.status.podIP}')

Record namespace, SERVICE_IP, Service port, target port, protocol, selector, Pod IPs, readiness, and EndpointSlice addresses. You will compare later steps against this baseline.


Test ClusterIP and Service DNS

From net-client, test the short DNS name and the ClusterIP on the Service port:

bash
kubectl exec -n svc-trouble-lab net-client -- curl -s -o /dev/null -w "%{http_code}\n" "http://web:80"

Sample output:

output
200

Test the ClusterIP directly:

bash
kubectl exec -n svc-trouble-lab net-client -- curl -s -o /dev/null -w "%{http_code}\n" "http://${SERVICE_IP}:80"

Sample output:

output
200

Test the fully qualified Service name. Kubernetes defines the full name as <service>.<namespace>.svc.<cluster-domain>. This kubeadm lab uses the default cluster.local domain. On clusters with a custom domain, read the search entries in the client Pod's /etc/resolv.conf and substitute that domain.

bash
kubectl exec -n svc-trouble-lab net-client -- cat /etc/resolv.conf
bash
kubectl exec \
  -n svc-trouble-lab net-client \
  -- curl \
  -s \
  -o /dev/null \
  -w "%{http_code}\n" "http://web.svc-trouble-lab.svc.cluster.local."

Sample output:

output
200

Kubernetes Service records use the configured cluster domain rather than requiring cluster.local.

Interpret the pair:

DNS name ClusterIP Direction
Resolves but both fail Fails Service routing, EndpointSlices, or backend
Both work internally Fails externally NodePort, LoadBalancer, or Ingress path
Intermittent Intermittent One or more backends are still marked Ready despite returning failures, or replicas have inconsistent application state

HTTP 200 on both short name and ClusterIP confirms in-cluster Service routing before you debug external exposure.


Diagnose EndpointSlice Backends

No addresses versus no ready endpoints

EndpointSlice addresses and readiness are separate checks. No addresses usually means the selector matches no Pods. Addresses with ready=false mean the Pods were selected but are not currently eligible for normal Service traffic.

bash
kubectl get endpointslice -n svc-trouble-lab -l kubernetes.io/service-name=web -o wide

Inspect each address and its ready condition:

bash
kubectl get endpointslice \
  -n svc-trouble-lab \
  -l kubernetes.io/service-name=web \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{" terminating="}{.conditions.terminating}{"\n"}{end}'

Sample healthy output:

output
192.168.5.48 ready=true terminating=
192.168.5.42 ready=true terminating=

When one readiness probe fails:

output
192.168.5.48 ready=false terminating=
192.168.5.42 ready=true terminating=

For a Service with publishNotReadyAddresses: true, Kubernetes treats matching EndpointSlice addresses as ready even when the corresponding Pods are not Ready.

If ENDPOINTS is empty or <none>, check:

  • Service spec.selector matches Pod labels
  • Pods live in the same namespace as the Service
  • The Service is not a headless or manual Endpoints special case you misread

When addresses exist but ready=false, fix readiness probes or terminating Pods instead of changing the selector. Use EndpointSlice commands for inspection. The legacy Endpoints API is deprecated from Kubernetes v1.33 onward.

Compare selectors and Pod labels

Every selector key-value pair must match Pod labels. Selectors match Pod template labels, not Deployment metadata labels outside the Pod template.

Print the Service selector:

bash
kubectl get service web -n svc-trouble-lab -o jsonpath='{.spec.selector}'

Sample output:

output
{"app":"web"}

List Pod labels in the same namespace:

bash
kubectl get pods -n svc-trouble-lab --show-labels

A Pod-template label change or a wrong Service selector removes all backends. Patch the selector to a non-matching value to reproduce an empty EndpointSlice:

bash
kubectl patch service web -n svc-trouble-lab --type=json -p='[{"op":"replace","path":"/spec/selector/app","value":"web-broken"}]'

Wait until no addresses remain. The EndpointSlice controller continuously evaluates Service selectors and stores matching backends in one or more EndpointSlices, so immediate reads can otherwise show the previous state.

bash
until [ "$(kubectl get endpointslice \
  -n svc-trouble-lab \
  -l kubernetes.io/service-name=web \
  -o jsonpath='{.items[*].endpoints[*].addresses[0]}' |
  wc -w)" -eq 0 ]; do
  sleep 1
done

Re-check EndpointSlices:

bash
kubectl get endpointslice -n svc-trouble-lab -l kubernetes.io/service-name=web

Sample output:

output
NAME        ADDRESSTYPE   PORTS     ENDPOINTS   AGE
web-k557g   IPv4          <unset>   <none>      45s

Traffic through the Service fails when no addresses are published:

bash
kubectl exec -n svc-trouble-lab net-client -- sh -c '
  curl -sS --max-time 3 http://web:80 >/dev/null
  rc=$?
  echo "curl_exit=$rc"
'

Sample output:

output
curl: (7) Failed to connect to web port 80
curl_exit=7

A dropped packet path can instead return:

output
curl: (28) Connection timed out after 3000 milliseconds
curl_exit=28

Restore the correct selector:

bash
kubectl patch service web -n svc-trouble-lab --type=json -p='[{"op":"replace","path":"/spec/selector/app","value":"web"}]'

Wait for both addresses to return:

bash
until [ "$(kubectl get endpointslice \
  -n svc-trouble-lab \
  -l kubernetes.io/service-name=web \
  -o jsonpath='{.items[*].endpoints[*].addresses[0]}' |
  wc -w)" -eq 2 ]; do
  sleep 1
done

After the controller reconciles, Service curls succeed again.

Test readiness removal and recovery

A Pod can be Running and still be NotReady. Normal Service routing uses ready endpoints only.

List Pod readiness:

bash
kubectl get pods -n svc-trouble-lab -l app=web

Remove the readiness marker on one Pod to simulate a failing probe:

bash
kubectl exec -n svc-trouble-lab "$BACKEND_POD" -- rm -f /tmp/healthy
bash
kubectl wait pod/"$BACKEND_POD" -n svc-trouble-lab --for=condition=Ready=false --timeout=60s

Inspect EndpointSlice conditions with the JSONPath command from earlier. EndpointSlice readiness maps to the corresponding Pod readiness condition for Pod-backed endpoints.

Service traffic may still succeed when other ready backends exist:

bash
kubectl exec -n svc-trouble-lab net-client -- curl -s http://web:80

Repeated requests can hit only healthy replicas until readiness recovers. Configure probe YAML in Kubernetes health probes. publishNotReadyAddresses: true is a special override for peer-discovery scenarios, not normal HTTP Services.

Restore readiness before continuing with the port tests:

bash
kubectl exec -n svc-trouble-lab "$BACKEND_POD" -- touch /tmp/healthy
bash
kubectl wait pod/"$BACKEND_POD" -n svc-trouble-lab --for=condition=Ready --timeout=60s

Diagnose Service Ports and the Backend Process

Verify port and targetPort

Clients connect to the Service port. Kubernetes forwards to targetPort on each selected Pod:

text
Client → Service port 80 → Pod targetPort 80

Confirm the mapping on the Service:

bash
kubectl get service web -n svc-trouble-lab -o jsonpath='port={.spec.ports[0].port} targetPort={.spec.ports[0].targetPort}{"\n"}'

Sample output:

output
port=80 targetPort=http

Patch targetPort to a port nothing listens on:

bash
kubectl patch service web -n svc-trouble-lab --type=json -p='[{"op":"replace","path":"/spec/ports/0/targetPort","value":8080}]'
bash
kubectl wait endpointslice \
  -n svc-trouble-lab \
  -l kubernetes.io/service-name=web \
  --for=jsonpath='{.ports[0].port}'=8080 \
  --timeout=60s

Service routing fails while direct Pod access on the real port still works:

bash
kubectl exec -n svc-trouble-lab net-client -- sh -c '
  curl -sS --max-time 3 http://web:80 >/dev/null
  rc=$?
  echo "curl_exit=$rc"
'

Sample output:

output
curl: (7) Failed to connect to web port 80
curl_exit=7
bash
kubectl exec -n svc-trouble-lab net-client -- curl -s -o /dev/null -w "%{http_code}\n" "http://${BACKEND_IP}:80"

Sample output:

output
200

That split points at targetPort or Service port mapping, not the application process. Restore the named target port:

bash
kubectl patch service web -n svc-trouble-lab --type=json -p='[{"op":"replace","path":"/spec/ports/0/targetPort","value":"http"}]'
bash
kubectl wait endpointslice -n svc-trouble-lab -l kubernetes.io/service-name=web --for=jsonpath='{.ports[0].port}'=80 --timeout=60s

Checklist when ports look correct but traffic still fails:

  • Client uses the Service port, not targetPort
  • Named targetPort exists on the Pod spec
  • Protocol matches (TCP versus UDP)
  • Multi-port Services use unique port names

Defining containerPort does not make an application listen on that port.

Test every Pod directly

Bypass the Service and curl each Pod IP plus target port from net-client:

bash
for POD in $(kubectl get pods -n svc-trouble-lab -l app=web -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'); do

POD_IP=$(kubectl get pod "$POD" -n svc-trouble-lab -o jsonpath='{.status.podIP}')

CODE=$(kubectl exec -n svc-trouble-lab net-client -- curl -s -o /dev/null -w '%{http_code}' --max-time 3 "http://${POD_IP}:80")

  printf '%s %s %s\n' "$POD" "$POD_IP" "$CODE"
done

Sample output:

output
web-6b5c766787-fhjs2 192.168.5.48 200
web-6b5c766787-nw7mx 192.168.5.42 200

This makes the intermittent-backend diagnosis reproducible rather than checking an arbitrary first Pod.

Interpret Pod IP versus Service results:

Pod IP test Service test Likely cause
Fails Fails Application, Pod port, or NetworkPolicy
Works Fails Selector, EndpointSlice, Service port, or dataplane path
Some Pods fail Intermittent One unhealthy backend or inconsistent Pod config

When direct Pod access fails, read application logs with kubectl logs, events, and describe before changing the Service.

Confirm the listening address

Traffic arrives at the Pod network namespace IP, not loopback inside the container. A process bound only to 127.0.0.1 is not reachable through the Pod IP or Service.

Inside a backend Pod, list listeners:

bash
kubectl exec -n svc-trouble-lab "$BACKEND_POD" -- netstat -tln

Sample output:

output
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN

0.0.0.0:80 accepts connections on the Pod IP. When the image lacks tools, use an ephemeral debug container — see kubectl debug Pods.


Check NetworkPolicy and the Service Data Plane

When Pod IP or Service tests time out instead of returning connection refused, inspect NetworkPolicy on both sides.

  • Egress from the client Pod namespace must allow traffic to the backend on the target port
  • Ingress to the backend Pod must allow traffic from the client on that port
  • Namespace and Pod selectors must match the actual client and server labels

Once both directions are isolated by policy, you need rules on source egress and destination ingress. Configure policies in Kubernetes NetworkPolicy.


Troubleshoot NodePort and LoadBalancer

Confirm ClusterIP routing from net-client before you debug external paths.

NodePort

Expose the Deployment as NodePort only after internal curls succeed:

bash
kubectl expose deployment web -n svc-trouble-lab --name=web-nodeport --type=NodePort --port=80 --target-port=http

Sample output:

output
service/web-nodeport exposed

Capture its allocated port and a node InternalIP:

bash
NODE_PORT=$(kubectl get service web-nodeport -n svc-trouble-lab -o jsonpath='{.spec.ports[0].nodePort}')
bash
NODE_NAME=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
bash
NODE_IP=$(kubectl get node "$NODE_NAME" -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}')

Test from the client Pod:

bash
kubectl exec -n svc-trouble-lab net-client -- curl -s -o /dev/null -w "%{http_code}\n" "http://${NODE_IP}:${NODE_PORT}"

Sample output:

output
200

With the default externalTrafficPolicy: Cluster, any reachable NodePort address can forward to ready endpoints across the cluster. When externalTrafficPolicy: Local is configured, the selected node must have an eligible local endpoint.

Kubernetes configures the allocated NodePort on every eligible node address and, by default, forwards it to the Service's ready endpoints. Node-local endpoint availability becomes a strict requirement with local external traffic policy.

A successful request from net-client verifies the in-cluster path to <NodeIP>:<NodePort>. It does not verify workstation routing, the node firewall, or any external network between the workstation and node.

Test from the workstation when NODE_IP is routable from it:

bash
curl -sS --connect-timeout 5 -o /dev/null -w '%{http_code}\n' "http://${NODE_IP}:${NODE_PORT}"

Sample output:

output
200

Run this from the workstation only when NODE_IP is routable from it. If the in-cluster NodePort request succeeds but this request fails, investigate the workstation-to-node route, node firewall, VirtualBox or cloud networking, and any external firewall rules.

LoadBalancer

LoadBalancer troubleshooting belongs after ClusterIP succeeds. Check external address assignment, controller events, health checks, and cloud firewall rules. Keep provider-specific debugging outside this portable workflow. For HTTP host and path routing, continue with expose services with Ingress after the Service path is healthy.


Service Troubleshooting Decision Flow and Symptoms

text
Can the backend Pod answer on PodIP:targetPort?
├─ No
│  ├─ Check application process and listening address
│  ├─ Check target port
│  ├─ Check Pod readiness and logs
│  └─ Check NetworkPolicy
└─ Yes
   ├─ Does EndpointSlice contain the Pod address?
   │  ├─ No: selector, labels, or namespace
   │  └─ Yes: check ready=false before blaming selector
   ├─ Does ClusterIP:port work?
   │  ├─ No: port mapping or Service data plane
   │  └─ Yes
   ├─ Does Service DNS work?
   │  ├─ No: DNS troubleshooting
   │  └─ Yes
   └─ Does external access work?
      └─ Check NodePort, LoadBalancer or Ingress
Symptom Likely cause First check
Empty EndpointSlice addresses Selector mismatch or Pods in another namespace Align selector with Pod labels; confirm namespace
Addresses present but ready=false Failing readiness probes or terminating Pods Fix probes or wait for termination to finish
ClusterIP fails, Pod IP works Wrong targetPort or Service port Correct port mapping on the Service
DNS fails, ClusterIP works Resolver or search path See DNS troubleshooting guide
Requests succeed and fail intermittently One backend remains advertised as Ready but the application is failing, replicas have inconsistent configuration, or the readiness probe is too weak Test each ready EndpointSlice address directly and compare Pod logs/configuration
Timeout from client Pod NetworkPolicy or CNI path Verify egress and ingress on port
Internal OK, external fails NodePort, LB, or Ingress layer Debug external exposure last

What's Next


References

Summary

You troubleshot a Kubernetes Service by working inside out: confirm the application listens on the Pod IP and target port, verify EndpointSlices publish addresses and ready conditions, then test ClusterIP and DNS from a client Pod before touching NodePort or LoadBalancer.

The failures that look alike from a browser often split cleanly in the cluster. Empty EndpointSlice addresses usually mean selector or namespace problems. Addresses with ready=false point at readiness or termination. Pod IP success with Service failure points at port mapping. ClusterIP success with DNS failure belongs in DNS troubleshooting, not Service YAML edits.

Keep a dedicated net-client Pod in the same namespace for reproducible curls. Record Service port, target port, selector, and EndpointSlice output before you patch anything, so you can tell whether the fix restored backends or only masked an external exposure issue.


Frequently Asked Questions

1. Why does my Service have no ready endpoints?

If EndpointSlices contain no addresses, the Service selector usually matches no Pods or the Pods are in another namespace. If addresses exist with ready=false, the Pods are NotReady or terminating. Inspect both addresses and endpoint conditions before changing the Service.

2. Can a Service work when all Pods are Running but not Ready?

No for normal ClusterIP routing. Running means the container started. NotReady means readiness probes failed, so matching Pods may still appear in EndpointSlices with ready=false and are excluded from normal Service traffic unless publishNotReadyAddresses is set on the Service.

3. What is the difference between Service port and targetPort?

Clients connect to the Service port on the ClusterIP or DNS name. Kubernetes forwards to targetPort on each selected Pod. A mismatch produces connection failures even when Pod IP and targetPort respond correctly.

4. Should I test a Service from my workstation first?

Test from a Pod inside the cluster first. Workstation curls to NodePort or LoadBalancer cannot tell you whether ClusterIP routing, EndpointSlices, or DNS failed before external exposure enters the path.

5. Does defining containerPort make the application listen on that port?

No. containerPort documents the port for humans and named targetPort references. The process must actually bind to the port and address that targetPort maps to.

6. When should I check NetworkPolicy for a Service failure?

When direct Pod IP and targetPort tests time out from a client Pod in the same namespace but ClusterIP routing also fails, or when only some client namespaces cannot connect. Verify both egress from the client and ingress to the backend on the required port.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)