Chapter 26 of 47 · Services and discovery

Headless Services: talking to one specific Pod

A normal Service hides its Pods behind one virtual IP and picks a Pod for you. Usually that is exactly what you want. But a database primary, a Kafka broker, or a peer in a cluster must be reached by name, individually. A headless Service gives every Pod its own DNS name instead.

When "any Pod" is not good enough

Fine with a normal Service

posts, comments, query: every copy is identical and stateless. Any Pod can answer any request.

Needs a specific Pod

  • A database: writes must go to the primary, reads may go to replicas.
  • Kafka, Cassandra, Redis Cluster, etcd: members must find each other by name.
  • gRPC clients that want every Pod address to balance requests themselves.

What headless means

One line: clusterIP: None. The Service gets no virtual IP, kube-proxy does nothing for it, and nothing load-balances. It only does one job: DNS.

Normal Service versus headless ServiceA normal Service answers DNS with one virtual IP and forwards to a Pod chosen for you. A headless Service answers DNS with every Pod IP, and the client connects to the one it wants. Normal (ClusterIP) client db · 10.96.5.9picks a Pod for you some db Pod (?) Headless (clusterIP: None) client DNS? db-headless"here are all 3 IPs"no virtual IP connects directly db-0 db-1 db-2 Left: the Service chooses. Right: the client chooses.

What DNS returns

$ nslookup db
Name:     db.default.svc.cluster.local
Address:  10.96.5.9            ← one virtual IP

One address. Which Pod you reach is decided per connection by kube-proxy.

Headless + StatefulSet = stable names

A Deployment's Pods get random names (db-7d9f8-x2kqp) that change on every replacement, so per-Pod DNS would be useless. A StatefulSet (chapter 38 goes deeper) is a controller like a Deployment, but for stateful apps: its Pods are numbered, and a replacement Pod gets the same name (and, in chapter 31, the same disk). Step through:

  1. db-0  Running   db-0.db-headless → 10.244.1.21

    Pods start one at a time, in order. db-0 first. It becomes the primary.

  2. db-0  Running   db-0.db-headless → 10.244.1.21
    db-1  Running   db-1.db-headless → 10.244.2.14
    db-2  Running   db-2.db-headless → 10.244.3.8

    db-1 and db-2 start after it and connect to db-0.db-headless to replicate.

  3. db-1  ✗ node-2 died

    A Pod is lost.

  4. db-1  Running   db-1.db-headless → 10.244.3.30   ← new IP, same name

    The StatefulSet recreates db-1, not a random name. DNS updates to the new IP. Anyone using the name db-1.db-headless still finds it.

The YAML

apiVersion: v1
kind: Service
metadata:
  name: db-headless
spec:
  clusterIP: None
  selector:
    app: db
  ports:
    - port: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
spec:
  serviceName: db-headless
  replicas: 3
  selector:
    matchLabels:
      app: db
  template:
    metadata:
      labels:
        app: db
    spec:
      containers:
        - name: postgres
          image: postgres:16
          ports:
            - containerPort: 5432
  # volumeClaimTemplates: ...   (chapter 31)

clusterIP: None

The one line that makes it headless. Everything else is a normal Service: a selector and ports.

Often you add a second, normal Service for the same Pods: a ClusterIP db-read that spreads read queries over all replicas, while writes use db-0.db-headless.

Using it from Node

// writes: always the primary
const primary = new Pool({ host: 'db-0.db-headless', port: 5432 });

// reads: any replica (a normal ClusterIP Service in front of all of them)
const replicas = new Pool({ host: 'db-read', port: 5432 });

// or: get every Pod address yourself
const { resolve4 } = require('node:dns/promises');
const ips = await resolve4('db-headless.default.svc.cluster.local');   // ['10.244.1.21', ...]

ClusterIP vs headless

ClusterIP (normal)Headless
clusterIPA virtual IPNone
DNS answersThe virtual IPAll ready Pod IPs
Load balancingYes, by kube-proxyNo; the client decides
Per-Pod namesNoYes, with a StatefulSet
Use forStateless appsDatabases, clustered systems, client-side balancing

Good to know

Quick check

Can you curl http://db-headless:5432 and expect load balancing like a normal Service?
Show answer

No. There is no virtual IP and no kube-proxy rule. The client gets a list of Pod IPs and, depending on the library, usually just uses the first. Use a normal Service when you want balancing.

Headless Services: common questions

What is a headless Service in Kubernetes?

A Service with clusterIP: None. It has no virtual IP and no load balancing; DNS returns the IPs of all ready Pods, and with a StatefulSet each Pod gets its own DNS name.