12.7 Service Endpoints and EndpointSlices
Alright, let’s pull back the curtain on the real magic trick: how your Service actually routes traffic. You’ve defined this abstract Service with a selector, but that’s just a declaration of intent. The actual, on-the-ground traffic cops are the Endpoints and EndpointSlices resources. If you don’t understand them, you’re flying blind when things go wrong.
Think of your Service as a VIP list for a club. The Endpoints resource is the actual, physical bouncer’s list, with the current addresses of who’s allowed in. When you create a Service with a selector like app=my-api, Kubernetes doesn’t just sit there. It constantly scans the cluster for Pods that match those labels. It then takes the IP addresses of those healthy Pods (where their readiness probes are passing) and populates a resource named exactly the same as your Service: the Endpoints resource.
You can see this for yourself. Create a simple Deployment and Service, then ask Kubernetes what it knows.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
Apply that, then run:
kubectl get endpoints nginx-service
You’ll see an output that looks shockingly like a list of Pod IPs and ports, because that’s exactly what it is.
NAME ENDPOINTS AGE
nginx-service 10.244.1.5:80,10.244.2.8:80 2m
This is the ground truth. Your ClusterIP is just a virtual IP (managed by kube-proxy on every node) that points to this list. If traffic isn’t routing, the very first thing you check is kubectl get endpoints. If that list is empty, your selector is wrong, your Pods aren’t ready, or they haven’t been assigned an IP yet. It’s the most direct “is anything even listening?” check you have.
The Old Guard: The Endpoints Resource
The original Endpoints resource is… fine. It gets the job done. But it has a problem, a very Kubernetes-scale problem: it’s a single, monolithic object. Every update to any Pod in the Service—a rollout, a scale-up, a Pod crashing—requires a full rewrite of the entire Endpoints object. For a Service managing hundreds of Pods, this is inefficient. It creates a lot of churn in the Kubernetes API and for every watcher of that object (which is every kube-proxy on every node). We can do better.
The New Hotness: EndpointSlices
Enter EndpointSlices, the scalable successor. Instead of one giant Endpoints list, a large Service is split across multiple EndpointSlice resources. By default, each EndpointSlice holds a maximum of 100 endpoints. This is a brilliant design. Now, when a single Pod dies, only the small EndpointSlice that contains it needs to be updated, not the entire set. It’s a classic divide-and-conquer strategy.
Kubernetes automatically manages these for you. If you have that same nginx-service from before, you can see them in action:
kubectl get endpointslices
You’ll see something like:
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
nginx-service-abc12 IPv4 80 10.244.1.5 5m
nginx-service-xyz98 IPv4 80 10.244.2.8 5m
Notice the clever, generated name? Each slice gets a unique suffix. This design is why you can have Services with thousands of Pods without melting the API server. It’s one of those behind-the-scenes optimizations that makes the platform actually work at scale.
When You Take Manual Control
Here’s where it gets interesting. Sometimes, you need to route traffic to something that isn’t a Pod in your cluster. Maybe an external database, a legacy API running on a bare-metal server, or another cluster altogether. The Service selector is useless here because there’s no Pod for it to find.
The solution? You manually create the Endpoints resource yourself. You define the exact IPs and ports, and then you create a Service without a selector. Kubernetes, seeing no selector, gives up on auto-population and trusts you to provide the corresponding Endpoints resource. The names must match.
# external-service.yaml
apiVersion: v1
kind: Service
metadata:
name: external-database-service
spec:
ports:
- protocol: TCP
port: 5432
targetPort: 5432
---
# external-endpoints.yaml
apiVersion: v1
kind: Endpoints
metadata:
name: external-database-service # <- Must match the Service name!
subsets:
- addresses:
- ip: 192.168.49.1 # Some external IP
ports:
- port: 5432
Now, any application in your cluster can talk to external-database-service:5432, and traffic will be seamlessly routed to that external IP. It’s a fantastic trick for gradually migrating services into Kubernetes. The key gotcha, of course, is that you’re now responsible for the life cycle of that Endpoints object. If the external IP changes, you have to update it yourself. No automatic healing here.
The Gotchas and The Glory
The main pitfall is assuming the Service is the source of truth. It’s not. It’s just the front door. The Endpoints/EndpointSlices are the foundation. Always check them first when debugging “my Service isn’t working.”
Another edge case: readiness probes are the gatekeepers for Endpoints. A Pod that fails its readiness check is yanked from the Endpoints list immediately. This is how we do graceful rollouts and handle temporary failures. But it means if your readiness probe is overly sensitive or misconfigured, you’ll have a perfectly healthy Pod that’s mysteriously not receiving any traffic. It’s the most common self-inflicted wound.
The best practice? Embrace EndpointSlices. They’re enabled by default in modern Kubernetes versions for a reason. They’re more efficient and the future of the API. For manual external services, be diligent. Consider using the ExternalName Service type for simple DNS aliases, but for specific IPs, the manual Endpoints method is your powerful, if slightly clunky, tool. Remember, the traffic goes where the Endpoints point, not where the Service wishes it would go.