StatefulSet
A Kubernetes workload controller designed for applications that require stable, persistent identity across pod restarts — such as databases, message queues, and distributed stores. Unlike Deployments, each StatefulSet pod gets a predictable name, dedicated storage, and starts/stops in a guaranteed sequential order.
StatefulSet — Running Stateful Apps in Kubernetes
Why Deployments Break for Databases
A standard Deployment treats all pods as identical and disposable. Pod names are random (postgres-7d9f8c-xkqzp), storage is not guaranteed to follow a pod, and all pods can start or die simultaneously.
For a PostgreSQL primary-replica setup at Zerodha, this is catastrophic:
+------------------------------------------+| Deployment (WRONG for databases) || || postgres-7d9f8c-xkqzp <- random name | <- If pod dies, new pod gets| postgres-7d9f8c-ab1cd <- random name | a different random name.| | Replicas lose their target.| Shared PVC: postgres-data | <- Two pods writing to the same| ^ both pods mount this | disk = data corruption+------------------------------------------+ +------------------------------------------+| StatefulSet (CORRECT for databases) || || postgres-0 <- stable, permanent name | <- Always the primary.| postgres-1 <- stable, permanent name | Replicas always know where| postgres-2 <- stable, permanent name | to replicate from.| || postgres-data-0 <- dedicated PVC | <- Each pod owns its own disk.| postgres-data-1 <- dedicated PVC | PVC survives pod restarts.| postgres-data-2 <- dedicated PVC |+------------------------------------------+The Three Guarantees StatefulSet Provides
1. Stable Network Identity
Each pod gets a permanent DNS name that never changes, even after restarts:
+------------------------------------------+| DNS record format: || || <pod-name>.<headless-svc>.<ns>.svc.cluster.local| || postgres-0.postgres-svc.production.svc.cluster.local| postgres-1.postgres-svc.production.svc.cluster.local| postgres-2.postgres-svc.production.svc.cluster.local+------------------------------------------+2. Ordered Startup and Shutdown
+------------+ +------------+ +------------+| postgres-0 | | postgres-1 | | postgres-2 || | | | | || Starts 1st | --> | Starts 2nd | --> | Starts 3rd || (primary) | | (Running?) | | (Running?) |+------------+ +------------+ +------------+ Shutdown order is reversed:postgres-2 stops -> postgres-1 stops -> postgres-0 stops (primary last)3. Persistent Volume Per Pod
Each pod gets its own PVC via volumeClaimTemplates. The PVC survives pod deletion — when the pod recreates, it reattaches to the same volume automatically.
A Real StatefulSet Manifest — Redis Cluster
apiVersion: apps/v1kind: StatefulSetmetadata: name: redis namespace: productionspec: serviceName: redis-headless # Must reference a Headless Service replicas: 3 selector: matchLabels: app: redis template: metadata: labels: app: redis spec: containers: - name: redis image: redis:7.2-alpine ports: - containerPort: 6379 volumeMounts: - name: redis-data mountPath: /data # Each pod mounts its own /data volume resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" volumeClaimTemplates: # This is what makes it a StatefulSet - metadata: name: redis-data spec: accessModes: ["ReadWriteOnce"] storageClassName: gp3-encrypted resources: requests: storage: 10Gi # Each of 3 pods gets its own 10Gi PVC # Total: 30Gi provisioned automaticallyHeadless Service — The Required Companion
StatefulSets require a Headless Service (clusterIP: None) to assign stable DNS records to each individual pod:
apiVersion: v1kind: Servicemetadata: name: redis-headless namespace: productionspec: clusterIP: None # "Headless" — no virtual IP, just DNS per pod selector: app: redis ports: - port: 6379 name: redis+------------------------------------------+ +------------------------------------------+| Regular Service (clusterIP: 10.96.0.5) | | Headless Service (clusterIP: None) || | | || DNS: redis-svc.production.svc... | | DNS: redis-0.redis-headless.production.. || Routes to ANY redis pod (random) | | DNS: redis-1.redis-headless.production.. || Good for stateless read balancing | | Routes to SPECIFIC pod (by ordinal) || | | Required for primary-replica topology |+------------------------------------------+ +------------------------------------------+RememberWithout the Headless Service, pods don't get their individual DNS records. The
serviceNamein your StatefulSet spec MUST exactly match the Headless Servicemetadata.name— a mismatch means pods start but DNS never resolves.
Managing StatefulSet Pods
# List all pods — notice the stable ordinal nameskubectl get pods -n production -l app=redis# NAME READY STATUS RESTARTS AGE# redis-0 1/1 Running 0 5d# redis-1 1/1 Running 0 5d# redis-2 1/1 Running 0 5d # List the auto-created PVCs (one per pod)kubectl get pvc -n production -l app=redis# NAME STATUS VOLUME CAPACITY# redis-data-redis-0 Bound pvc-3a8f2c1d-... 10Gi# redis-data-redis-1 Bound pvc-5c2e4b1a-... 10Gi# redis-data-redis-2 Bound pvc-7d3f9c2b-... 10Gi # Scale up — redis-3 will be created with its own PVCkubectl scale statefulset redis --replicas=4 -n production # Rolling update (in reverse order: 2, 1, 0)kubectl rollout status statefulset/redis -n productionStatefulSet vs Deployment — Decision Guide
| Factor | Use Deployment | Use StatefulSet |
|---|---|---|
| App stores state to disk | No | Yes |
| Pods need unique identity | No | Yes |
| All pods are interchangeable | Yes | No |
| Startup order matters | No | Yes |
| Examples | API servers, web frontends | PostgreSQL, Kafka, Elasticsearch, Redis Cluster, ZooKeeper |
Troubleshooting Common StatefulSet Problems
| Problem | Symptom | Fix |
|---|---|---|
Pod stuck in Pending |
redis-1 never starts |
redis-0 is not yet in Running state — fix the first pod before others proceed |
| PVC not binding | redis-data-redis-0 stuck in Pending |
StorageClass missing or wrong AZ — check kubectl describe pvc redis-data-redis-0 events |
| Pod recreated but lost data | Data empty after restart | Pod reattached to wrong PVC — verify PVC name matches pod ordinal exactly |
| Headless Service DNS not resolving | nslookup redis-0.redis-headless fails |
serviceName in StatefulSet spec doesn't match the Headless Service name |
| Scale-down left orphan PVCs | PVCs remain after replicas: 1 |
StatefulSet never auto-deletes PVCs on scale-down — delete manually after confirming data is safe |
Common MistakeRunning a database as a Deployment with a single shared PVC. When the pod reschedules to a different node, the
ReadWriteOncePVC cannot follow to a different node, leaving the pod permanently stuck inPending.
SecurityNever run StatefulSet pods as
rootinside the container. A pod breakout on a database StatefulSet with root access can corrupt data or exfiltrate the entire volume. Always setsecurityContext.runAsNonRoot: trueandreadOnlyRootFilesystem: trueon StatefulSet containers.
TipWhen upgrading StatefulSets (e.g., Redis version bump), use
updateStrategy: RollingUpdatewithpartition: 2to canary-test the upgrade on only the last pod first. Once verified healthy, setpartition: 0to roll out to all pods.
Frequently Asked Questions
Why can't I just run a database as a regular Kubernetes Deployment?
A Deployment treats pods as interchangeable — it assigns random suffixes to pod names, can recreate pods in any order, and by default all replicas share the same PersistentVolumeClaim template, meaning they'd all try to mount the same volume. A StatefulSet instead gives each pod a stable, predictable name (`app-0`, `app-1`, `app-2`), a dedicated PersistentVolumeClaim per pod that persists across rescheduling, and deploys/scales pods one at a time in order — properties databases and quorum-based systems like Kafka or etcd depend on.
What's a common StatefulSet mistake teams make in production?
Assuming StatefulSet gives you data replication or backup automatically — it doesn't; it only guarantees stable identity and storage, the replication logic (e.g., Postgres streaming replication, Kafka partition replicas) is still the application's job to configure. Also, scaling down a StatefulSet doesn't delete its PersistentVolumeClaims by default, which is usually what you want (data isn't lost) but surprises people who expect a clean teardown.