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 EndpointSliceNothing 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: TCPPods 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.ioNAMESPACE NAME ADDRESSTYPE PORTS ENDPOINTS AGE
default external-backend-pk59r IPv4 9000 192.168.1.50 12sEach 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 yamlThe 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-backendThe 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: truekubernetes.io/service-nameis what ties the slice to the Service. The slice name itself can be anything.endpointslice.kubernetes.io/managed-bytells 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[].namemust 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-backendNAME ADDRESSTYPE PORTS ENDPOINTS AGE
external-backend-1 IPv4 9000 192.168.1.50 3s
external-backend-pk59r IPv4 9000 192.168.1.50 2mTwo 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-backendWarning: 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 3sThe 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 backendThen 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
httpand left the slice port as"". The slice still listed the address, butkubectl describe svcshowed an emptyEndpoints:line and curl gotCould not connect to server. Renaming the slice port tohttpfixed it immediately. kubectl get endpointsnow says NotFound. That is expected: nothing creates v1 Endpoints for a Service without a selector anymore.kubectl describe svcreads 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 samekubernetes.io/service-namelabel. - Health is your job. Nobody probes an external backend. If it goes down, set
ready: falseor 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.iolabel. - Apply your own EndpointSlice first, then delete the Endpoints. The mirrored slice dies with its owner.
- Match
ports[].nameto the Service port name, including the empty name.
