Skip to content

Migrating from Endpoints to EndpointSlice: Fix the v1.33+ Deprecation Warning

Last updated on October 27, 2026

Updated October 2026. The first version of this post showed the swap from Endpoints to EndpointSlice and stopped there. This one adds what I ran into when doing it on real clusters: how to find every hand-made Endpoints object, what happens to the slice Kubernetes mirrors from it, and the port-name mismatch that makes a correct-looking slice route nothing. Everything below was run on Kubernetes 1.36.3 (k3s) against a plain HTTP server outside the cluster.

Since Kubernetes 1.33 every command that touches a v1 Endpoints object prints this:

Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice

Nothing is broken. The Endpoints API still works, and the Kubernetes team itself says the deprecation policy means it will probably never go away completely, so you do not have to rush. The warning is there to move new code and manifests to EndpointSlice, which kube-proxy and every modern controller already use. If you only ever see it after typing kubectl get endpoints, switch the command to kubectl get endpointslice and you are done. If it shows up when you apply your own manifests, read on.

Where hand-made Endpoints come from

For a normal Service with a selector, Kubernetes writes both objects itself and you never touch them. Hand-made Endpoints only exist for Services without a selector, usually to give an in-cluster name to something outside the cluster: a database on a VM, a legacy app, a NAS. A typical pair looks like this:

apiVersion: v1
kind: Service
metadata:
  name: external-backend
spec:
  ports:
    - port: 9000
      protocol: TCP
      targetPort: 9000
---
apiVersion: v1
kind: Endpoints
metadata:
  name: external-backend
subsets:
  - addresses:
      - ip: 192.168.1.50
    ports:
      - port: 9000
        protocol: TCP

Pods reach it as http://external-backend:9000, and an Ingress or Gateway route can point at the Service the same way. None of that changes during the migration, because routes point at the Service, not at the Endpoints.

1. Find what needs migrating

Kubernetes copies every hand-made Endpoints object into an EndpointSlice and labels the copy. That label is the quickest inventory:

kubectl get endpointslice -A -l endpointslice.kubernetes.io/managed-by=endpointslicemirroring-controller.k8s.io
NAMESPACE   NAME                     ADDRESSTYPE   PORTS   ENDPOINTS      AGE
default     external-backend-pk59r   IPv4          9000    192.168.1.50   12s

Each line is one Endpoints object you created yourself. To cross-check, list the Services without a selector:

kubectl get svc -A -o json | jq -r '.items[] | select(.spec.selector == null and .spec.type != "ExternalName") | "\(.metadata.namespace)/\(.metadata.name)"'

default/kubernetes always shows up here. It belongs to the API server, leave it alone.

2. Look at the mirrored slice

kubectl get endpointslice -l kubernetes.io/service-name=external-backend -o yaml

The interesting part of the output:

metadata:
  labels:
    endpointslice.kubernetes.io/managed-by: endpointslicemirroring-controller.k8s.io
    kubernetes.io/service-name: external-backend
  ownerReferences:
    - kind: Endpoints
      name: external-backend

The mirrored slice is owned by your Endpoints object. Delete the Endpoints and the garbage collector deletes the slice with it, so you cannot just keep the mirror and drop the original. You need a slice of your own.

3. Create your own EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: external-backend-1
  labels:
    kubernetes.io/service-name: external-backend
    endpointslice.kubernetes.io/managed-by: cluster-admins
addressType: IPv4
ports:
  - name: ""
    port: 9000
    protocol: TCP
endpoints:
  - addresses:
      - 192.168.1.50
    conditions:
      ready: true
  • kubernetes.io/service-name is what ties the slice to the Service. The slice name itself can be anything.
  • endpointslice.kubernetes.io/managed-by tells everyone (and every controller) who owns the slice. Use any value of your own, just not one of the controller names you saw above.
  • ports[].name must be exactly the name of the Service port. The Service above has an unnamed port, so the slice uses "". More on this in the gotchas.
  • No ownerReferences, so nothing deletes this slice except you.
kubectl apply -f external-backend-slice.yaml
kubectl get endpointslice -l kubernetes.io/service-name=external-backend
NAME                     ADDRESSTYPE   PORTS   ENDPOINTS      AGE
external-backend-1       IPv4          9000    192.168.1.50   3s
external-backend-pk59r   IPv4          9000    192.168.1.50   2m

Two slices with the same address are fine for a while, kube-proxy just sees the same backend twice.

4. Delete the old Endpoints

kubectl delete endpoints external-backend
kubectl get endpointslice -l kubernetes.io/service-name=external-backend
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
endpoints "external-backend" deleted from default namespace
NAME                 ADDRESSTYPE   PORTS   ENDPOINTS      AGE
external-backend-1   IPv4          9000    192.168.1.50   3s

The mirrored slice is gone, yours stays. Check the traffic from a pod:

kubectl run curl --image=curlimages/curl:8.17.0 --restart=Never --command -- sleep 3600
kubectl exec curl -- curl -s http://external-backend:9000/
hello from the external backend

Then remove the Endpoints from your manifests or Helm chart and keep only the Service and the EndpointSlice. Otherwise the next deploy creates the Endpoints again, along with its warning and a new mirrored slice.

Gotchas

  • The port name has to match. I gave the Service port the name http and left the slice port as "". The slice still listed the address, but kubectl describe svc showed an empty Endpoints: line and curl got Could not connect to server. Renaming the slice port to http fixed it immediately.
  • kubectl get endpoints now says NotFound. That is expected: nothing creates v1 Endpoints for a Service without a selector anymore. kubectl describe svc reads the slices and still shows the address.
  • Deleting Endpoints first breaks traffic. The mirrored slice goes with it. Always apply your own slice before you delete anything.
  • Several backends? Put them all in endpoints: of one slice (up to 1000 addresses per slice), or split them into more slices with the same kubernetes.io/service-name label.
  • Health is your job. Nobody probes an external backend. If it goes down, set ready: false or remove the address, the same as you had to with Endpoints.

Takeaways

  • The warning is a deprecation notice, not a removal date. Nothing stops working on 1.33+.
  • Only Services without a selector need manual work. Find them with the endpointslicemirroring-controller.k8s.io label.
  • Apply your own EndpointSlice first, then delete the Endpoints. The mirrored slice dies with its owner.
  • Match ports[].name to the Service port name, including the empty name.

Links

Published inKubernetesLinux