04-state-storage-resources

StatefulSets: Identity, Ordering and What They Don't Do

Stable names, per-replica volumes and ordered rollouts — plus the honest limits that make operators the better answer for databases.

September 13, 2026
kubernetesstatefulsetsvolumeclaimtemplatesheadless-serviceoperatorsdatabases

When Replicas Aren't Interchangeable

A Deployment assumes its pods are identical and disposable. Any replica can serve any request; names are random; order doesn't matter. For a stateless API that's exactly right.

Some workloads need the opposite. A database replica set has a primary and followers. A Kafka broker owns specific partitions. A distributed cache assigns key ranges by node. These need identity — a name that survives rescheduling, so peers can find each other and so the right data lands on the right pod.

That's a StatefulSet.

Three Guarantees

yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres-headless
  replicas: 3
  selector:
    matchLabels: { app: postgres }
  template:
    metadata:
      labels: { app: postgres }
    spec:
      containers:
        - name: postgres
          image: postgres:17
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: [ReadWriteOnce]
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 20Gi

1. Stable names

Pods are numbered from zero and keep their names forever:

text
postgres-0    postgres-1    postgres-2

Delete postgres-1 and its replacement is also postgres-1 — not a new random suffix. Compare with a Deployment, where every replacement gets a fresh name like api-7d4f8b9c5-x2kfp.

2. Stable network identity

Paired with a headless Service (clusterIP: None, from the Services guide), each pod gets its own DNS record:

text
postgres-0.postgres-headless.production.svc.cluster.local
postgres-1.postgres-headless.production.svc.cluster.local

So postgres-1 can address postgres-0 directly and reliably. That's how replicas find their primary, and how brokers find peers — cluster membership without a service-discovery system.

yaml
apiVersion: v1
kind: Service
metadata:
  name: postgres-headless
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
    - port: 5432

3. Per-replica storage

volumeClaimTemplates is the field that matters most. Each replica gets its own PVC, generated from the template:

text
data-postgres-0    data-postgres-1    data-postgres-2

This is the direct answer to the RWO problem from the previous guide. A Deployment gives every replica the same PVC, which fails with a multi-attach error on RWO storage. A StatefulSet gives each replica a separate volume — and because postgres-1 always keeps its name, it always reattaches to its own data.

💡

Notice that volumeClaimTemplates sits at the StatefulSet level, not inside spec.template. That's deliberate: the pod template describes each pod, while the claim template describes per-pod storage the controller creates and manages independently.

Ordering Guarantees

By default a StatefulSet is strictly sequential:

  • Scale up: postgres-0 must be Running and Ready before postgres-1 is created.
  • Scale down: highest ordinal first — postgres-2, then postgres-1.
  • Rolling update: reverse order, one at a time, waiting for Ready between each.

This is what lets a primary initialise before followers try to replicate from it.

Sequencing is also why a StatefulSet can get stuck: if postgres-0 never becomes Ready, nothing else is created. A single failing readiness probe blocks the entire set — and it looks like Kubernetes doing nothing rather than reporting an error.

yaml
spec:
  podManagementPolicy: Parallel    # start/stop all at once

Parallel is appropriate when replicas don't depend on each other's startup order but still need stable identity and per-replica storage.

For updates, partition enables staged rollouts:

yaml
spec:
  updateStrategy:
    rollingUpdate:
      partition: 2      # only ordinals >= 2 are updated

Set partition: 2 on a 3-replica set and only postgres-2 updates. Verify it, then lower the partition to roll the rest — a canary within the set.

Check yourself

A 3-replica StatefulSet is scaled to 5. `postgres-3` is stuck in Pending because no node has capacity. What happens to `postgres-4`?

PVCs Outlive the StatefulSet

Deliberately, and it catches people out:

  • Scale down from 3 to 1 and the PVCs for postgres-1 and postgres-2 are not deleted.
  • Delete the StatefulSet entirely and its PVCs remain.

The reasoning is sound — the data is usually the point, and automatic deletion on a scale-down typo would be catastrophic. But it means storage accumulates silently, and scaling back up reattaches the old data rather than starting fresh.

bash
kubectl get pvc -l app=postgres          # orphaned claims persist here

Newer Kubernetes offers persistentVolumeClaimRetentionPolicy to automate this for whenDeleted and whenScaled, but the default remains retain. Check what your cluster's version supports before relying on it.

⚠️

Combine this with reclaimPolicy: Delete on the StorageClass and you have a sharp edge: PVCs survive a StatefulSet deletion, but deleting those PVCs destroys the underlying disks immediately. Tidying up orphaned claims is a data-destroying operation. Check what's in them first.

The Honest Limitation

A StatefulSet gives you identity, ordering and per-replica storage. That is all it gives you.

It does not give you:

  • Replication — configuring a primary and followers is your job
  • Failover — promoting a replica when the primary dies
  • Backups and point-in-time recovery
  • Version upgrades that respect the database's own procedure
  • Connection routing that follows the current primary

Every one of those is essential to running a database, and every one is something you'd be implementing yourself. That's a substantial project, not a configuration task.

This is why the practical answer for databases on Kubernetes is usually an operator — CloudNativePG for PostgreSQL, Strimzi for Kafka, and similar. An operator is a controller that encodes operational knowledge: it watches a custom resource and handles failover, backups and upgrades using the same reconciliation model from phase 1. You declare "a 3-node Postgres cluster with daily backups" and the operator does the work.

A managed database service is also a legitimate answer. "We didn't run the database in Kubernetes" is a perfectly good architectural decision, and worth saying out loud in an interview rather than assuming everything must live in the cluster.

Deployment or StatefulSet?

NeedUse
Stateless app, interchangeable replicasDeployment
Shared storage all replicas write toDeployment + an RWX volume
Per-replica storage, stable namesStatefulSet
Peers addressing each other directlyStatefulSet + headless Service
A production databaseAn operator, or a managed service

Check yourself

What does volumeClaimTemplates provide that a Deployment referencing a PVC cannot?

What's Next

Storage is handled. The next guide covers the resource settings that determine whether your pod gets scheduled at all — and the two limit types that fail in completely different ways.