Skip to main content
RunBook Academy

← All break/fix scenarios in Kubernetes

intermediatekubernetes-endpoint~25 min

Service has no endpoints

Reported symptoms

  • Six calling services began failing against `checkout.prod.svc.cluster.local` at 14:07; the failures are immediate rather than slow, and every caller in every namespace is affected equally
  • The ingress controller started returning 503 for the `/checkout` path at the same minute
  • The checkout Deployment reports `6/6` ready and `kubectl rollout status` says the rollout succeeded
  • The checkout Pods are processing their queue and their own application dashboards are green
  • `kubectl get pods -n prod -l app=checkout` returns `No resources found`, which was escalated as "the Pods are gone"
  • `kubectl get pods -n prod` shows six checkout Pods, all Running and 1/1 Ready
  • DNS is fine: the Service name resolves to its ClusterIP from every Pod that was tested

Evidence

  • · `kubectl describe service checkout -n prod` reports `Endpoints: <none>`
  • · `kubectl get endpointslices -n prod -l kubernetes.io/service-name=checkout` returns nothing
  • · `kubectl get service checkout -n prod -o jsonpath='{.spec.selector}'` shows `{"app":"checkout"}`
  • · `kubectl get pods -n prod --show-labels` shows the running Pods carrying `app.kubernetes.io/name=checkout` and no `app` label at all
  • · `kubectl get deployment checkout -n prod -o jsonpath='{.spec.selector.matchLabels}'` shows the same new label set
  • · `kubectl get deployment checkout -n prod -o jsonpath='{.metadata.creationTimestamp}'` shows the Deployment was created at 14:07 today, not when the service was first shipped
  • · The change that shipped at 14:06 is titled "adopt the recommended Kubernetes labels" and touches only the application repository
  • · The Service manifest in the platform repository has not been modified in eleven months
Diagnosis and resolutionclick to reveal

Root cause

The Service still selects `app: checkout`. The Pods no longer carry that label. A change adopting the recommended `app.kubernetes.io/*` label set renamed the labels on the Deployment, and because a Deployment's `spec.selector` is immutable the tooling could not edit the object in place - it deleted and recreated it. The recreate is why the transition was instantaneous rather than gradual: the old Pods carrying `app: checkout` went away in the same second the new ones carrying `app.kubernetes.io/name: checkout` appeared, and the Service's endpoint count went from six to zero with nothing in between. The Service was never touched, because it lives in a different repository owned by a different team, and nothing in Kubernetes connects the two objects. A selector is a contract expressed as a string match, with no foreign key, no referential integrity and no validation: the API server will accept a Service whose selector matches nothing, a Deployment whose Pods match no Service, and will report both objects healthy. Every status surface the two teams watched was telling the truth. The Deployment did roll out. The Pods are ready. The application is processing work. The only object in the cluster that knew the contract had been broken was the EndpointSlice, and nothing was watching it.

Remediation

There are two fixes and they cost different things, so choose deliberately rather than reaching for whichever comes to mind first. Patching the Service selector to `app.kubernetes.io/name: checkout` restores traffic in the time it takes kube-proxy to reprogram, restarts nothing, and is the right call while callers are down - but it edits an object that a reconciler owns, so the next sync from the platform repository reverts it and the outage returns, usually with nobody watching. If you take that path, treat landing the same change in the platform repository as part of the incident and not as follow-up work, with a named owner and a deadline inside the same shift. The alternative is to add `app: checkout` back to the Deployment's Pod template as an additional label rather than a rename. `spec.selector` is immutable but `spec.template.metadata.labels` is not, so adding a label is legal as long as the selector still matches; the cost is a full rollout, and the benefit is that both repositories stay authoritative and both selectors match at once, which is exactly the state a label migration should pass through. Whichever you pick, do not restart or scale the Deployment hoping to shake the endpoints loose - the Pods are healthy and restarting them recreates them with the same labels.

Verification

Confirm the endpoints first, then the traffic, then the durability of the fix - in that order, because each one can pass while the next one fails. `kubectl get endpointslices -n prod -l kubernetes.io/service-name=checkout` must list six addresses with `ready: true`, and `kubectl describe service checkout -n prod` must show them under `Endpoints`. Then prove reachability from a client rather than from the object: curl the Service name from a Pod in a calling namespace, since the whole incident is a case of every object reporting health while nothing could connect. Confirm the ingress path returns 200 again for `/checkout`, because that caller reached the Service by a different route and is the one that noticed first. Then the check most incidents of this shape skip: force a reconcile from the platform repository and re-read the endpoints. If the fix was a live patch to a managed object, this is the step where it disappears, and finding that out during verification is very much better than finding it out at the next scheduled sync. Finally, prove the new alert can fail: point a test Service at a selector that matches nothing in a staging namespace and confirm it fires.

Prevention

Alert on Services with zero ready endpoints. That single rule converts this entire failure class from an outage into a warning, and it is derivable from data the cluster already publishes; a Service that has had endpoints and now has none is unambiguous, and a Service that has never had any is worth knowing about too. Second, make label migrations additive. Add the new label, cut every selector over to it, verify, and only then remove the old one - a rename is atomic on the object you edit and manual on every object that refers to it, which is the shape of change that always leaves something behind. Third, stop treating `kubectl rollout status` as a deployment gate. It answers a question about the ReplicaSet and says nothing about whether anything can reach the result; a pipeline step that asserts the Service has the expected number of ready endpoints after the rollout would have failed this change in seconds. Fourth, keep the Service and the workload that backs it in one place, or derive both selectors from one templated value, so that a label change cannot be approved by reviewers who can only see one half of the contract. And record the general rule somewhere the next reviewer will read it: in Kubernetes, anything joined by labels is joined by a string, and no component will tell you when the string stops matching.

Reported symptoms

At 14:07 six services in four namespaces started failing against checkout.prod.svc.cluster.local. The ingress controller began returning 503 for /checkout in the same minute. The failures are immediate: the callers are not waiting for a timeout, they are being refused.

The checkout team sees none of this. Their Deployment reports 6/6 ready. kubectl rollout status says the rollout completed successfully. Their Pods are consuming from their queue, their latency panel is flat, and their error rate is zero, because nothing is reaching them to fail.

The first responder ran the obvious command and got a result that sent everyone in the wrong direction:

$ kubectl get pods -n prod -l app=checkout
No resources found in prod namespace.

That was escalated as “the checkout Pods are gone”, which produced ten minutes of work on scheduling, node capacity and quota. The Pods are not gone. Without the label selector, the same command lists six of them, Running and Ready.

DNS was ruled out early and correctly: the Service name resolves to its ClusterIP from every Pod anyone tested.

The change log for the day contains one deployment at 14:06. It is titled “adopt the recommended Kubernetes labels”, it was reviewed and approved, and it touches only the application repository.

Evidence provided

Read-only / Safethe Service exists, has an IP, and has nothing behind it
$ kubectl describe service checkout -n prod | grep -E 'Selector|Endpoints|IP:'
Selector:          app=checkout
IP:                10.96.71.204
Endpoints:         <none>

Illustrative output

Read-only / Safenot an empty slice - no slice at all
$ kubectl get endpointslices -n prod -l kubernetes.io/service-name=checkout
No resources found in prod namespace.

Illustrative output

Read-only / Safehealthy Pods, and no app label
$ kubectl get pods -n prod --show-labels | grep checkout
checkout-7f4b96c8d4-4rlq2   1/1   Running   0   14m   app.kubernetes.io/instance=checkout,app.kubernetes.io/name=checkout,pod-template-hash=7f4b96c8d4
checkout-7f4b96c8d4-8wnkd   1/1   Running   0   14m   app.kubernetes.io/instance=checkout,app.kubernetes.io/name=checkout,pod-template-hash=7f4b96c8d4
checkout-7f4b96c8d4-nq6vt   1/1   Running   0   14m   app.kubernetes.io/instance=checkout,app.kubernetes.io/name=checkout,pod-template-hash=7f4b96c8d4

Illustrative output

Read-only / Safethe Deployment is fourteen minutes old
$ kubectl get deployment checkout -n prod -o jsonpath='{.metadata.creationTimestamp}'; echo; kubectl get deployment checkout -n prod -o jsonpath='{.spec.selector.matchLabels}'
2026-08-18T14:07:03Z
{"app.kubernetes.io/instance":"checkout","app.kubernetes.io/name":"checkout"}

Illustrative output

Read-only / SafeDNS is not the problem
$ kubectl exec -n orders deploy/orders -- getent hosts checkout.prod.svc.cluster.local
10.96.71.204    checkout.prod.svc.cluster.local

Illustrative output

Work the evidence before reading on

The Deployment is healthy, the Pods are healthy, DNS resolves, and the Service has an IP. Work these in order.

  1. The Service’s selector and the Pods’ labels are both in the evidence above. Compare them character by character. Which Pods, anywhere in the cluster, would that selector match?
  2. The Deployment’s creationTimestamp is fourteen minutes old, but the workload has been in production for a year. What kind of change deletes and recreates a Deployment rather than updating it, and why would the tooling have had no choice?
  3. Callers are being refused immediately rather than timing out. What does that tell you about which of the five layers - DNS, Service, EndpointSlice, Pod IP, application port - is still working?

Before continuing, answer the question that decides the fix: which of the two objects is wrong, the Service or the Deployment? Note that “wrong” here is a question about ownership and intent, not about the API, because the API accepted both of them.

Root cause

1. The selector matches nothing

The Service selects app: checkout. The Pods carry app.kubernetes.io/name=checkout and app.kubernetes.io/instance=checkout and nothing else. There is no Pod in the prod namespace with an app label, so the EndpointSlice controller has nothing to put in a slice, and it does not create one.

That is the entire fault. Everything else on the page is a consequence.

2. Why the change was instantaneous

A Deployment’s spec.selector is immutable. The migration changed the labels the Deployment selects on, which the API server will not accept as an update, so the tooling did the only thing available to it: it deleted the Deployment and created a new one.

Deleting a Deployment deletes its ReplicaSets, which deletes their Pods. The six Pods carrying app: checkout disappeared at 14:07:03 and six Pods carrying the new labels appeared immediately afterwards. The Service’s endpoint count went from six to zero and stayed there.

This is why the incident has no gradual phase, no partial failure and no window in which somebody might have noticed a rising error rate. It also explains the timing precisely enough to identify the change, which is the one thing the evidence gives you for free.

3. The contract nothing enforces

The Service and the Deployment do not reference each other. There is no owner reference, no foreign key, no validating admission on this relationship, and no warning printed by any of the commands involved. Both objects are internally consistent and both are reported healthy.

Consider what each team could see:

SurfaceWhat it saidWas it lying?
kubectl rollout statusrollout succeededNo - the ReplicaSet did converge
Pod readiness6/6 readyNo - the containers are healthy
Application dashboardsgreenNo - the application is fine
Service objectexists, has a ClusterIPNo - it does exist
EndpointSliceabsentThis was the only signal

Nobody misread anything. The information required to see the fault existed in exactly one place, and it was a place neither team had a reason to look at during a deploy.

Resolution

  1. Establish which object is authoritative before editing either one. The Service lives in the platform repository and the Deployment in the application repository; the fix has to be applied where the object is owned, or it will not survive.
  2. Decide between the two repairs explicitly and say which one you are doing. The live Service patch is instant and creates drift; adding the legacy label back to the Pod template is durable and costs a rollout. Both are correct answers to different questions.
  3. If you patch the Service: kubectl patch service checkout -n prod --type=merge -p with the new selector, then watch the EndpointSlice appear. kube-proxy reprograms from the slice, so the recovery is measured in seconds and needs no Pod to move.
  4. If you patch the Deployment: add app: checkout to spec.template.metadata.labels as an additional label. Do not touch spec.selector, which is immutable - adding a Pod label is legal precisely because the existing selector still matches.
  5. Do not restart, scale or roll the Deployment in the hope of dislodging the endpoints. The Pods are healthy and every replacement carries the same labels; a restart costs capacity and changes nothing.
  6. Confirm the callers recover before declaring the incident over. Six services and an ingress path were failing, and the endpoints reappearing is not the same statement as traffic flowing.
  7. Land the corresponding change in the repository that owns the object you patched, inside the incident. This is the step that decides whether the outage recurs.
  8. Write the label migration down as a two-release plan before anyone attempts it again: both labels present, selectors moved, old label removed. The change that caused this tried to do all three at once.

Verification

  1. The EndpointSlice exists and is populated. kubectl get endpointslices -n prod -l kubernetes.io/service-name=checkout lists six addresses, and each endpoint shows ready: true rather than merely being present.
  2. kubectl describe service checkout -n prod lists the addresses under Endpoints. This is the same fact from the other side, and it is the one a responder will check first next time.
  3. A caller can connect. Curl the Service name from a Pod in one of the affected namespaces. Every object on this page reported health throughout the outage, so an object-level check is not evidence of reachability.
  4. The ingress path returns 200 for /checkout. That caller reached the Service by a different route and noticed the failure first, so it is the one that confirms the recovery is complete rather than partial.
  5. The fix survives a reconcile. Force a sync from the platform repository and re-read the endpoints. If the repair was a live patch that has not yet been landed, this is where it vanishes - which is exactly the point of checking now rather than later.
  6. The zero-endpoint alert fires when it should. Point a test Service at a selector that matches nothing in a staging namespace and confirm the alert appears. An alert that has only ever been silent has not been tested.
  7. The old label is still in place if you took the additive path. Confirm both app=checkout and app.kubernetes.io/name=checkout select the same six Pods, which is the state the migration should have passed through in the first place.

Prevention

  • Alert on Services with zero ready endpoints. This is the highest-value rule in the whole scenario. It costs one query, it fires within a minute of the fault, and it names the broken object directly instead of leaving six calling teams to work out whose problem it is.
  • Migrate labels additively, over two releases. Add the new label, move every selector to it, verify, then remove the old one. A rename is atomic on the object you edit and manual on everything that refers to it, and nothing will tell you what you missed.
  • Do not let kubectl rollout status be the deployment gate. It is a statement about the ReplicaSet. A pipeline step that asserts the Service has the expected number of ready endpoints after the rollout would have failed this change before any caller noticed.
  • Review the contract, not the diff. The change that caused this was correct, well-reviewed and complete within its own repository. It was unreviewable there, because the object it broke was not in it.
  • Prefer one source for the selector. Where the Service and the workload are templated together, derive both from a single value so that they cannot disagree. Where they cannot be templated together, say so in both files, next to the label.
  • Teach the string-match rule. Anything joined by labels in Kubernetes is joined by exact string equality, within one namespace, with no component watching the join. Once an operator holds that, this entire family of faults becomes one hypothesis instead of five.