ClusterIP & Headless
Summary
- A ClusterIP Service publishes one virtual IP that load-balances to backend pods; a headless Service publishes the pod IPs directly.
- Insight: You go headless when the caller needs to see individual pods - for stable identity, sharding, or client-side balancing.
- Key Concepts: ClusterIP, headless (
clusterIP: None), DNS A/AAAA records, StatefulSet identity, SRV records. - When to Use: ClusterIP for stateless request/response; headless for databases, StatefulSets, and peer-aware systems.
- Limitations: Headless has no VIP, so there is no built-in load balancing and no single stable IP to firewall.
Recipe
Use a normal ClusterIP Service by default.
Add a headless Service only when a client must address specific pods.
# Standard ClusterIP: get a VIP + DNS
kubectl expose deployment web --port=80 --target-port=8080
# Headless: no VIP, pod IPs returned in DNS
kubectl apply -f headless-service.yamlThe deciding question: does the caller need one address or all addresses?
Working Example
A StatefulSet with a headless Service gives each pod a stable DNS name.
apiVersion: v1
kind: Service
metadata:
name: cassandra
spec:
clusterIP: None
selector:
app: cassandra
ports:
- name: cql
port: 9042
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: cassandra
spec:
serviceName: cassandra
replicas: 3
selector:
matchLabels:
app: cassandra
template:
metadata:
labels:
app: cassandra
spec:
containers:
- name: cassandra
image: cassandra:5.0
ports:
- name: cql
containerPort: 9042The serviceName: cassandra field ties the StatefulSet to the headless Service.
Each pod now resolves at cassandra-0.cassandra.default.svc.cluster.local, and so on.
kubectl run tmp --rm -it --image=busybox:1.36 --restart=Never -- \
nslookup cassandra.default.svc.cluster.localThat lookup returns one A record per Ready pod, letting a client connect to the whole ring.
Deep Dive
How ClusterIP works
The control plane allocates a VIP from the Service CIDR and holds it for the life of the Service.
kube-proxy programs kernel rules so packets to the VIP are DNATed to a Ready endpoint.
DNS returns exactly one record - the VIP - so the client is unaware of individual pods.
This is the right default for stateless HTTP and gRPC backends.
How headless works
Setting clusterIP: None tells the control plane to skip VIP allocation.
CoreDNS then answers the Service name with one A/AAAA record per Ready endpoint.
There are no kube-proxy DNAT rules, because there is no VIP to translate.
The client receives the full list and decides which pod to talk to.
Stable per-pod identity
A headless Service paired with a StatefulSet yields per-pod DNS: <pod>-<ordinal>.<service>.
That name survives reschedules even though the pod IP changes.
Systems like Kafka, ZooKeeper, and Postgres replicas rely on this to find peers.
SRV records and ports
For named ports, CoreDNS also serves SRV records, exposing port and target together.
Clients that discover peers dynamically can query SRV to learn both host and port.
This is useful for clustered software that bootstraps from DNS.
ClusterIP allocation
The ClusterIP comes from the Service CIDR configured on the API server.
You can request a specific IP with spec.clusterIP, but it must fall inside that range and be free.
The field is immutable after creation, which is why switching between ClusterIP and headless means recreating the Service.
Dual-stack clusters can assign both an IPv4 and IPv6 ClusterIP through ipFamilyPolicy and ipFamilies.
Choosing between the two
Reach for ClusterIP whenever the client just wants "a healthy backend" and does not care which one.
Reach for headless whenever the client's logic depends on knowing the individual members.
Databases, quorum systems, and shard-aware clients almost always fall in the second bucket.
Everything stateless and request-scoped falls in the first.
Gotchas
DNS caching can mask membership changes - a client that caches a headless result may keep hitting a dead pod until the TTL expires.
Keep application-side DNS TTL handling honest, or use a client that re-resolves.
A headless Service does no load balancing, so if you expected even spreading you will get whatever your client does.
Publishing not-ready pods is opt-in: set publishNotReadyAddresses: true only when peers must discover each other during startup, and understand you may route to unready pods.
An empty DNS answer usually means no pods are Ready or the selector is wrong - check EndpointSlices first.
Do not firewall a headless Service by VIP; there is none, so write policy against pod labels instead.
Alternatives
Normal ClusterIP: pick this for stateless services that just need a load-balanced entry point.
Headless + StatefulSet: pick this for stateful, peer-aware systems needing stable identity.
Headless + client-side load balancing: pick this for gRPC clients that want to spread requests across all backends themselves.
ExternalName: pick this to alias an out-of-cluster host behind a cluster DNS name without any proxying.
FAQs
When should I set clusterIP: None?
When the caller must address individual pods - StatefulSets, databases, or client-side load balancing over gRPC.
Does a headless Service load balance? No. It only returns pod IPs in DNS. Any balancing is up to the client.
Can I convert a ClusterIP Service to headless later?
clusterIP is immutable, so you delete and recreate the Service rather than editing it in place.
Why do I see unready pods in DNS?
You likely set publishNotReadyAddresses: true, or the pods flap between Ready and not-Ready. Review readiness probes.
How do I address pod number two directly?
Use its StatefulSet DNS name, <statefulset>-1.<service>.<namespace>.svc.cluster.local.
Can a headless Service still have named ports and SRV records? Yes. Named ports produce SRV records that let clients discover both the host and port of each peer from DNS.
Does a headless Service use kube-proxy? No VIP means no kube-proxy DNAT rules; routing is entirely client-driven off the DNS answers.
Related
- Why Pods Need Services
- Services Basics
- Life of a Request
- NodePort & LoadBalancer
- EndpointSlices
- Services Best Practices
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).