Sign in
← Kubernetes: debugging
7 min · medium · Lesson 6 of 12

The Service isn't responding: selector mismatch

Why an empty endpoints list is the main symptom, how to check the selector against the Pod's labels, and which side to fix.

The Deployment is rolled out, the Pods are Running and Ready, the Service is created. You hit the Service and the client gets Connection refused. The app logs are empty: the requests simply never arrive.

A common cause behind this picture: the Service selector doesn't match the Pod labels. In this lesson you'll learn to spot that mismatch in two commands and fix it without taking the app down.

How a Service finds Pods

A Service is a Kubernetes object that gives a group of Pods a stable address (ClusterIP) and a DNS name. It doesn't know the Pods themselves. It only knows the selection rule: the spec.selector field, a set of key=value pairs.

The EndpointSlice controller looks at which Pods have labels that match this selector, and writes their IPs into an EndpointSlice object. It only looks in the namespace the Service lives in. Then kube-proxy, a component on every node, reads the EndpointSlice and sets up network rules: packets to the ClusterIP go to the IPs of specific Pods.

If the selector doesn't match, the EndpointSlice has no addresses and the packet has nowhere to go. On a cluster with kube-proxy in iptables mode (as in this lesson's lab), the client gets an immediate refusal:

wget: can't connect to remote host (10.96.215.189): Connection refused

The matching rule

A Pod makes it into a Service if it has every pair from the selector with the same value. The comparison is exact: no substrings, no regular expressions, and a Service has no operators like In.

Take the selector {app: webapp, tier: frontend}:

The last case is the trickiest. The app key matches, your eye catches that and stops reading.

The key indicator: EndpointSlice

kubectl get endpointslice the first command when a Service fails IP, ready=true selector matched, dig into ports <unset> no Pod matched IP, ready=false Pod found, readiness failed
One command, three outcomes, and from there you dig in different places.

When a Service isn't responding, the first thing to check is its EndpointSlice. Every slice has a kubernetes.io/service-name label with the Service's name, that's what you search by:

kubectl get endpointslice -l kubernetes.io/service-name=webapp

If the selector didn't match anyone, the slice exists but has no addresses in it:

NAME           ADDRESSTYPE   PORTS     ENDPOINTS   AGE
webapp-2pz7m   IPv4          <unset>   <unset>     16s

The ENDPOINTS column shows all the addresses found, including addresses of Pods that haven't passed the readiness check yet. Traffic doesn't go to those Pods. To see which addresses are ready, add a column with the conditions.ready field:

kubectl get endpointslice -l kubernetes.io/service-name=webapp \
  -o custom-columns='IP:.endpoints[*].addresses[0],READY:.endpoints[*].conditions.ready'
IP                      READY
10.244.0.5,10.244.0.6   true,true

That gives you three outcomes. No addresses: look for a mismatch between the selector and the labels (or there are no matching Pods at all). Addresses present but false: the Pods were found, the problem is in the readiness check. Addresses present and true: the selector is fine, dig into ports and network policies.

This same information used to be checked through kubectl get endpoints webapp. The v1 Endpoints object has been deprecated since Kubernetes 1.33, and kubectl warns about it:

Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
NAME     ENDPOINTS   AGE
webapp   <none>      16s

The command still works, but its ENDPOINTS column only shows ready addresses. If no Pod is Ready, the column is empty, and that case is easy to mistake for a selector mismatch.

kubectl describe svc webapp also shows ready addresses, in the Endpoints: line. It's empty when there's a mismatch.

How to compare the selector and the labels

The Service's selector:

kubectl get svc webapp -o jsonpath='{.spec.selector}'
{"app":"webapp","tier":"front-end"}

The Pods' labels:

kubectl get pods --show-labels
NAME                      READY   STATUS    RESTARTS   AGE   LABELS
webapp-5b7fd9946b-ncmnh   1/1     Running   0          17s   app=webapp,pod-template-hash=5b7fd9946b,tier=frontend
webapp-5b7fd9946b-tg9lc   1/1     Running   0          17s   app=webapp,pod-template-hash=5b7fd9946b,tier=frontend

Now compare the pairs one by one. app=webapp is on both sides. But tier=front-end on the Service and tier=frontend on the Pod don't match. The pod-template-hash label is added by the Deployment itself, it doesn't affect the match as long as it isn't in the selector.

There's also a quick way: select Pods with the same selector the Service has.

kubectl get pods -l app=webapp,tier=front-end

The answer No resources found confirms that no Pod fits this selector.

Cross-check: reach the Pod directly

Make sure the app itself is alive. Find one Pod's IP and hit it directly from a throwaway Pod:

kubectl get pod -l app=webapp -o jsonpath='{.items[0].status.podIP}'

kubectl run probe --image=busybox:1.36.1 --rm -it --restart=Never -- \
  wget -qO- -T 5 http://<POD_IP>:8080

The port here is containerPort, the one the container listens on, not the Service's port. Without :8080, wget would go to port 80 and get Connection refused, even though the app is fine.

If the Pod's IP answers but the Service doesn't, the cause is the selector or the Service's ports. The app has nothing to do with it.

In a namespace under Pod Security restricted, this kubectl run won't go through: admission will answer Forbidden, because the probe Pod has no securityContext. The error text lists the fields you need to set.

Selector, port, or network policy

From the outside these defects all look the same: "the service doesn't work". The EndpointSlice is what tells them apart.

What the client seesAddresses in the EndpointSliceCause
Connection refusednoneselector doesn't match the labels
Connection refusedpresent, ready=falsePods haven't passed the readiness check
Connection refusedpresent, ready=truetargetPort doesn't match the port the container listens on
timeoutpresent, ready=truea NetworkPolicy is blocking the traffic

The client can't tell the first three rows apart. That's why you check the EndpointSlice first: it tells you right away which side to dig into. On a cluster with a different dataplane, the "What the client sees" column may look different. The "Addresses in the EndpointSlice" column doesn't depend on that.

How it's fixed

The mismatch gets removed from one of the two sides, and the sides aren't equally good options.

Fixing the Service's selector is one command, and the EndpointSlice fills in within seconds:

kubectl patch svc webapp -p '{"spec":{"selector":{"tier":"frontend"}}}'

This command only changes the tier key, the rest of the selector's keys stay as they were. If an extra key needs to go away entirely, pass it null in a merge patch:

kubectl patch svc webapp --type=merge -p '{"spec":{"selector":{"<key>":null}}}'

kubectl patch here is an emergency measure. In production, a Service usually comes from Helm, Kustomize, or Argo CD, and a manual edit drifts from the manifest in Git. Argo CD with self-heal enabled will revert the object to the state from Git (docs). The permanent fix goes into the manifest or the chart's values and ships through the normal pipeline.

Fixing Pod labels through the Deployment is harder. An existing Deployment's spec.selector field can't be changed. The selector ties the Deployment to its ReplicaSet and Pods. A new selector wouldn't see the old ReplicaSets, and they'd be left orphaned (docs). The API responds like this:

The Deployment "webapp" is invalid:
* spec.selector: Invalid value: {"matchLabels":{"app":"webapp","tier":"front-end"}}: field is immutable

Labels in the Pod template (spec.template.metadata.labels) can be changed, but they must still cover the Deployment's selector. Adding a new label works. Changing a value that's part of the selector doesn't:

The Deployment "webapp" is invalid: spec.template.metadata.labels: Invalid value: {"app":"webapp","tier":"front-end"}: `selector` does not match template `labels`

So to change the Pod labels that are in the selector, the Deployment has to be recreated. Without downtime, that's done like this: bring up a new Deployment alongside it with the right labels, wait for it to be Ready, switch the Service over, and only then delete the old one.

That gives a practical rule: on a mismatch, you usually fix the Service. Exception: the Pod labels are wrong in meaning, say app: web instead of the project's accepted app: webapp. Then you fix the labels in the Deployment, even if it means recreating it.

It's best not to keep the app version in a Service selector. Otherwise every release needs a Service edit, and while Pods of two versions live side by side during the rollout, only one of them gets traffic. It's a different story when traffic is deliberately switched between two Deployments, as in the no-downtime approach above. There, the version pair in the Service selector is exactly what does the switching.

Common mistakes

Rewriting live Pods' labels. The command kubectl label pods ... --overwrite looks like a quick fix. If the rewritten label is part of the Deployment's selector, the Pods fall out of their ReplicaSet: its selector no longer sees them. The ReplicaSet immediately creates a replacement, and now you have twice as many Pods, half of them orphaned. The Service only works through those orphaned Pods, and the Deployment's new replicas never make it in.

If the label isn't part of the Deployment's selector, the Pods stay in the ReplicaSet and the Service works for now. But the Pod template hasn't changed: any new Pod after a restart or a rollout comes up with the old label and doesn't make it into the Service.

Checking Pods in the wrong namespace. kubectl get pods --show-labels without -n shows the namespace from your current context. A Service only looks for Pods in its own namespace, so compare labels in that same namespace the Service lives in.

Assuming an empty EndpointSlice is always a bug. A Service can have no selector at all and have its EndpointSlice filled in by hand, for example to point traffic at a database outside the cluster. Kubernetes doesn't touch that kind of slice on its own.

How to avoid it

Pull labels and selectors from one place, instead of writing them by hand in two files.

A Helm template from helm create already has a <chart>.selectorLabels helper. It gets used in both the Deployment's selector and the Service's selector.

In Kustomize there's a labels field with includeSelectors: true for this:

labels:
- pairs:
    app: webapp
  includeSelectors: true

The old commonLabels field does the same thing, but it's deprecated. kubectl prints Warning: 'commonLabels' is deprecated. Please use 'labels' instead.

After rolling out a new Service, it's worth checking that it has ready addresses. Right after apply the Pods aren't Ready yet, so in CI you first wait for the rollout, then print the ready addresses. Empty output means the rollout is broken. kubectl get itself returns exit code 0 on empty output, so the CI step fails on grep -q . instead:

kubectl rollout status deployment/webapp --timeout=120s
kubectl get endpointslice -l kubernetes.io/service-name=webapp \
  -o jsonpath='{range .items[*].endpoints[?(@.conditions.ready==true)]}{.addresses[0]}{"\n"}{end}' \
  | grep -q . || { echo "Service webapp has no ready addresses"; exit 1; }

Homework

Practice the theory on your own cluster: 1 step with a check. Set up the cluster with the Kubernetes locally track, the tasks live in the k8s-practice repository.

./run.sh start k8s-service-selector-mismatch     # set up the starting state
./run.sh verify k8s-service-selector-mismatch    # check the current step

In short

What you'll need for the task

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 1 lab for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson k8s-debug-06-service-selector-mismatch