Deployments treat Pods as interchangeable cattle. But some workloads — databases, message queues, distributed consensus systems — need stable identity: a fixed name, persistent storage that follows the Pod, and ordered startup. That's what StatefulSets provide.

1. The Problem StatefulSets Solve

Consider a 3-node PostgreSQL cluster with streaming replication:

  • Node 0 is the primary (accepts writes)
  • Nodes 1 and 2 are replicas (read-only, stream WAL from primary)
  • Each node has its own data directory (can't share storage)
  • Replicas need to find the primary by a stable DNS name
  • If a Pod restarts, it must get the same storage back

A Deployment can't do this. It gives Pods random names (web-8b2c-xk4j9), shares no identity across restarts, and provides no ordering guarantees.

StatefulSet Guarantees

GuaranteeDeploymentStatefulSet
Pod namesRandom suffix (web-8b2c-xk4j9)Ordinal index (db-0, db-1, db-2)
Stable network identity❌ (IP changes on reschedule)✅ (fixed DNS name per Pod)
Persistent storageShared or ephemeralDedicated PVC per Pod, reattached on reschedule
Startup orderAll at onceSequential: 0 → 1 → 2 (configurable)
Shutdown orderAny orderReverse: 2 → 1 → 0
StatefulSet = stable identity + stable storage + ordered operations. Use them when your application is not fungible — when each instance has a role, needs its own disk, or must discover peers by name.

2. StatefulSet Anatomy

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres-headless    # ← REQUIRED: headless Service name
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16
          ports:
            - containerPort: 5432
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
          env:
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
  volumeClaimTemplates:             # ← PVC per Pod
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 50Gi
  podManagementPolicy: OrderedReady # ← sequential (default)
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      partition: 0                  # ← for canary updates

Key Differences from Deployment

FieldPurposeNotes
serviceNameNames the headless Service for DNSRequired — must exist
volumeClaimTemplatesCreates a unique PVC per PodPVCs persist even if Pod/STS is deleted
podManagementPolicyOrderedReady (sequential) or ParallelParallel skips ordering for faster startup
updateStrategy.partitionOnly update Pods with ordinal ≥ partitionEnables canary updates

3. Stable Network Identity

Pod Naming

Pods are named {statefulset-name}-{ordinal}:

# StatefulSet "postgres" with replicas=3 creates:
postgres-0
postgres-1
postgres-2

# If postgres-1 dies, it's rescheduled as postgres-1 (same name, same PVC)

Headless Service & DNS

A headless Service (ClusterIP: None) is required. It doesn't load-balance — instead, it creates DNS records for each Pod individually:

# Headless Service definition:
apiVersion: v1
kind: Service
metadata:
  name: postgres-headless
spec:
  clusterIP: None           # ← This makes it headless
  selector:
    app: postgres
  ports:
    - port: 5432

DNS Records Created

postgres-headless.default.svc.cluster.local → returns ALL Pod IPs (A records) postgres-0 postgres-0.postgres-headless .default.svc.cluster.local postgres-1 postgres-1.postgres-headless .default.svc.cluster.local postgres-2 postgres-2.postgres-headless .default.svc.cluster.local Each Pod gets a stable DNS name: {pod-name}.{service-name}.{namespace}.svc.cluster.local This DNS name stays the same even if the Pod is rescheduled to a different node
# From any Pod in the cluster:
nslookup postgres-0.postgres-headless.default.svc.cluster.local
# → 10.244.1.5 (Pod IP — may change on reschedule, but DNS name stays)

# Replicas can find the primary by name:
# postgresql.conf: primary_conninfo = 'host=postgres-0.postgres-headless ...'
Stable DNS ≠ stable IP. The DNS name is stable (always resolves to the correct Pod), but the underlying IP may change when a Pod is rescheduled. Applications should connect by DNS name, not IP. This is why headless Services exist — they give each Pod its own discoverable identity.
You often need TWO Services for a StatefulSet: (1) a headless Service for internal peer discovery (clusterIP: None), and (2) a regular ClusterIP/LoadBalancer Service for client traffic (load-balanced across all Pods). The headless Service is for the StatefulSet's serviceName field.

4. Persistent Storage — volumeClaimTemplates

The volumeClaimTemplates field creates a unique PVC for each Pod, named {template-name}-{statefulset-name}-{ordinal}:

# For StatefulSet "postgres" with volumeClaimTemplate named "data":
kubectl get pvc
# NAME              STATUS   VOLUME         CAPACITY   STORAGECLASS
# data-postgres-0   Bound    pv-abc123      50Gi       fast-ssd
# data-postgres-1   Bound    pv-def456      50Gi       fast-ssd
# data-postgres-2   Bound    pv-ghi789      50Gi       fast-ssd

Storage Lifecycle

STS created PVCs created (one per Pod) Pod deleted/ rescheduled Same PVC reattached ✓ STS deleted PVCs SURVIVE ⚠️ Must delete manually
PVCs are never automatically deleted. Deleting a StatefulSet or scaling down does NOT delete the PVCs. This is a safety feature — your data survives even if you accidentally delete the StatefulSet. You must manually delete PVCs to reclaim storage.

PVC Retention Policy (K8s 1.27+)

# New: control PVC lifecycle explicitly
spec:
  persistentVolumeClaimRetentionPolicy:
    whenDeleted: Delete    # Delete PVCs when STS is deleted
    whenScaled: Retain     # Keep PVCs when scaling down (default)
PolicywhenDeletedwhenScaled
Default (safe)RetainRetain
Clean up on deleteDeleteRetain
Aggressive cleanupDeleteDelete
After scaling down a StatefulSet from 5 to 3, PVCs data-postgres-3 and data-postgres-4 still exist. If you scale back up to 5, those Pods will reattach to their original PVCs — data is preserved. This is intentional for databases: scale down during low traffic, scale up later without data loss.

5. Ordered Operations

podManagementPolicy: OrderedReady (default)

OperationOrderCondition
Scale up0 → 1 → 2 → ...Each Pod must be Running+Ready before next starts
Scale down... → 2 → 1 → 0Each Pod must be fully terminated before next stops
Update... → 2 → 1 → 0 (reverse)Each updated Pod must be Ready before next is updated

Why? Many distributed systems require this:

  • A primary (index 0) must start first before replicas can connect
  • Shutting down the primary last ensures failover happens cleanly
  • Rolling updates from highest ordinal first = update replicas before primary

podManagementPolicy: Parallel

All Pods start/stop simultaneously — no ordering. Use when Pods are independent (e.g., each is a standalone cache shard that doesn't need to discover peers at startup).

spec:
  podManagementPolicy: Parallel
  # All Pods created at once during scale-up
  # Faster, but no startup ordering guarantee

6. Update Strategies

RollingUpdate (default)

Updates Pods in reverse ordinal order (highest first): 2 → 1 → 0. Each Pod must become Ready before the next is updated.

Partition — Canary Updates

The partition parameter is powerful: only Pods with ordinal ≥ partition are updated. Pods below the partition keep the old version.

spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      partition: 2    # Only postgres-2 gets the new version

# After verifying postgres-2 is healthy:
# Set partition: 1 → updates postgres-2, postgres-1
# Set partition: 0 → updates all (full rollout)
Partition enables staged rollouts for databases. Update one replica first (canary), verify replication works with the new version, then proceed. If it fails, only one replica is affected — set partition back to 3 and the old version stays on 0, 1.

OnDelete Strategy

spec:
  updateStrategy:
    type: OnDelete
  # Pods are NOT automatically updated
  # You must manually delete each Pod to trigger an update
  # Gives maximum control for critical stateful workloads

7. When to Use StatefulSets vs Deployments

Use StatefulSet WhenUse Deployment When
Each Pod needs its own persistent diskPods share no state or use shared storage
Pods need stable DNS names for peer discoveryPods are behind a load-balanced Service
Startup/shutdown order mattersPods are interchangeable
Database, Kafka, ZooKeeper, Elasticsearch, etcdWeb servers, API servers, workers, caches
Many teams avoid StatefulSets for managed databases (use RDS, Cloud SQL, etc.). StatefulSets shine for: self-managed databases that must run on K8s, distributed systems (Kafka, Elasticsearch, CockroachDB), and any workload where each instance has a distinct role or persistent state.

Summary

ConceptKey Point
Stable identityPods named {sts}-{ordinal}, preserved across rescheduling
Headless ServiceRequired — gives each Pod its own DNS name
volumeClaimTemplatesUnique PVC per Pod, survives Pod deletion and STS deletion
Ordered operationsScale up 0→N, scale down N→0, update N→0
PartitionOnly update Pods ≥ partition ordinal (canary for stateful apps)
PVC retentionPVCs never auto-deleted (safety). Must delete manually or configure policy.

📝 Quiz: StatefulSets

Q1: A StatefulSet "redis" has 3 replicas. What are the Pod names and in what order do they start?

Pod names: redis-0, redis-1, redis-2.
Start order (with OrderedReady): redis-0 starts first and must be Running+Ready before redis-1 starts, then redis-2.

Q2: You delete the StatefulSet "postgres" with kubectl delete sts postgres. What happens to the PVCs?

PVCs survive. Deleting a StatefulSet does NOT delete its PVCs (unlike Pods which are deleted). This is a safety feature to protect data. You must manually delete PVCs: kubectl delete pvc data-postgres-0 data-postgres-1 data-postgres-2.

Q3: What DNS name would Pod "kafka-2" in namespace "messaging" with headless Service "kafka-svc" get?

kafka-2.kafka-svc.messaging.svc.cluster.local
Format: {pod-name}.{service-name}.{namespace}.svc.cluster.local

Q4: You have a 5-replica StatefulSet and set partition: 3 with a new image. Which Pods get updated?

Only Pods with ordinal ≥ 3: pod-3 and pod-4 get the new image. Pods pod-0, pod-1, pod-2 keep the old image. This is used for canary testing of stateful workloads.

Q5: Why does a StatefulSet require a headless Service (clusterIP: None) rather than a normal Service?

A normal Service provides a single virtual IP and load-balances across Pods — you can't address individual Pods. A headless Service creates individual DNS A records for each Pod, enabling direct peer-to-peer communication. StatefulSet Pods need to find each other by name (e.g., replica connects to postgres-0 specifically).

Q6: You scale a StatefulSet from 5 down to 3, then later scale back up to 5. What storage do Pods 3 and 4 get?

They reattach to their original PVCs (data-sts-3 and data-sts-4) which were retained during scale-down. All data from before the scale-down is still there. This is the key value of StatefulSet storage — data persists through scale operations.