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
| Guarantee | Deployment | StatefulSet |
|---|---|---|
| Pod names | Random 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 storage | Shared or ephemeral | Dedicated PVC per Pod, reattached on reschedule |
| Startup order | All at once | Sequential: 0 → 1 → 2 (configurable) |
| Shutdown order | Any order | Reverse: 2 → 1 → 0 |
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
| Field | Purpose | Notes |
|---|---|---|
serviceName | Names the headless Service for DNS | Required — must exist |
volumeClaimTemplates | Creates a unique PVC per Pod | PVCs persist even if Pod/STS is deleted |
podManagementPolicy | OrderedReady (sequential) or Parallel | Parallel skips ordering for faster startup |
updateStrategy.partition | Only update Pods with ordinal ≥ partition | Enables 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
# 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 ...'
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
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)
| Policy | whenDeleted | whenScaled |
|---|---|---|
| Default (safe) | Retain | Retain |
| Clean up on delete | Delete | Retain |
| Aggressive cleanup | Delete | Delete |
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)
| Operation | Order | Condition |
|---|---|---|
| Scale up | 0 → 1 → 2 → ... | Each Pod must be Running+Ready before next starts |
| Scale down | ... → 2 → 1 → 0 | Each 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)
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 When | Use Deployment When |
|---|---|
| Each Pod needs its own persistent disk | Pods share no state or use shared storage |
| Pods need stable DNS names for peer discovery | Pods are behind a load-balanced Service |
| Startup/shutdown order matters | Pods are interchangeable |
| Database, Kafka, ZooKeeper, Elasticsearch, etcd | Web servers, API servers, workers, caches |
Summary
| Concept | Key Point |
|---|---|
| Stable identity | Pods named {sts}-{ordinal}, preserved across rescheduling |
| Headless Service | Required — gives each Pod its own DNS name |
| volumeClaimTemplates | Unique PVC per Pod, survives Pod deletion and STS deletion |
| Ordered operations | Scale up 0→N, scale down N→0, update N→0 |
| Partition | Only update Pods ≥ partition ordinal (canary for stateful apps) |
| PVC retention | PVCs 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?
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?
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.localFormat:
{pod-name}.{service-name}.{namespace}.svc.cluster.localQ4: You have a 5-replica StatefulSet and set partition: 3 with a new image. Which Pods get updated?
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?
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?
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.