A headless service in Kubernetes doesn’t get a cluster IP, which is weirdly its main point.
Let’s see one in action. Imagine we have a simple Nginx deployment.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
Now, let’s create a headless service for it.
apiVersion: v1
kind: Service
metadata:
name: nginx-headless-service
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
clusterIP: None # This is the magic
The clusterIP: None is what makes it headless. Normally, Kubernetes assigns a stable virtual IP to a service, and DNS queries for the service name resolve to this IP. But with None, that doesn’t happen. Instead, DNS queries for the service name will return the IP addresses of the individual pods backing that service.
Let’s check our pods:
kubectl get pods -l app=nginx -o wide
You might see something like:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATETIME
nginx-deployment-abcdefg-hijkl 1/1 Running 0 2m 10.244.1.5 worker-node-1
nginx-deployment-xyz1234-mnopq 1/1 Running 0 2m 10.244.2.7 worker-node-2
nginx-deployment-uvw9876-rstuv 1/1 Running 0 2m 10.244.1.6 worker-node-1
Now, from inside another pod in the cluster (let’s say a busybox pod for testing), we can try to resolve the headless service.
kubectl run -it --rm busybox --image=busybox -- sh
Inside the busybox shell:
nslookup nginx-headless-service
The output will look like this:
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: nginx-headless-service
Address 1: 10.244.1.5
Address 2: 10.244.2.7
Address 3: 10.244.1.6
Notice how it’s not returning a single cluster IP, but the actual IP addresses of the three Nginx pods. This is direct pod discovery. The DNS lookup for nginx-headless-service.your-namespace.svc.cluster.local (or just nginx-headless-service if you’re in the same namespace) is handled by CoreDNS (or kube-dns), which sees the clusterIP: None and instead queries the Kubernetes API for endpoints associated with that service, returning the IPs of the matching pods.
This is incredibly useful for stateful applications like databases or distributed systems where each instance needs to know about its peers directly. If you’re running a Cassandra cluster, for instance, each Cassandra node might need to know the IP addresses of all other Cassandra nodes to form a ring. A headless service allows your application to discover these peer IPs dynamically without relying on a load balancer. You can also use this for client-side load balancing strategies or for specific discovery mechanisms that require direct access to individual instances.
A common misconception is that headless services offer no load balancing at all. While they don’t provide the network-level load balancing of a standard service (where traffic is distributed across pods by kube-proxy), they enable application-level or client-side load balancing by providing the raw list of pod IPs. The client application then decides how to distribute requests among these IPs.
The next logical step is exploring how to leverage these direct pod IPs within your application, perhaps by implementing a simple round-robin or random selection strategy in your client code.