StatefulSet
A StatefulSet is the Kubernetes API object that manages the deployment and scaling of a set of Pods with stable identities, stable persistent storage, and ordered, predictable lifecycle. Where a Deployment treats its replicas as interchangeable cattle, a StatefulSet treats them as named, ordinally-indexed individuals:
web-0,web-1,web-2. Each Pod is created in order, gets its ownPersistentVolumeClaimcloned from a template, retains a stable DNS name backed by a companion Headless Service, and is replaced on update in reverse ordinal order — the new Pod inheriting the old Pod’s identity and storage. The Kubernetes documentation captures the guarantee concisely: “manages the deployment and scaling of a set of Pods, and provides guarantees about the ordering and uniqueness of these Pods” (kubernetes.io — StatefulSet). This guarantee is the entire reason StatefulSet exists. It is what makes Kubernetes capable of running distributed databases (Postgres operators, MySQL/Vitess, MongoDB, Elasticsearch), consensus systems (etcd, Zookeeper, Consul), and partitioned services (Kafka, Cassandra, RabbitMQ clusters) without giving up the orchestration benefits of the platform. Conversely, StatefulSet is the wrong answer for nearly anything stateless — its ordered semantics are slower, more brittle, and more operationally demanding than Deployment’s; reach for it only when you genuinely need identity.
Mental Model
flowchart TB SS[StatefulSet 'web'<br/>replicas=3<br/>serviceName='nginx'<br/>volumeClaimTemplates] HS["Headless Service 'nginx'<br/>clusterIP: None<br/>selector: app=nginx"] POD0["Pod web-0<br/>identity: web-0<br/>PVC: data-web-0<br/>DNS: web-0.nginx.ns.svc.cluster.local"] POD1["Pod web-1<br/>identity: web-1<br/>PVC: data-web-1<br/>DNS: web-1.nginx.ns.svc.cluster.local"] POD2["Pod web-2<br/>identity: web-2<br/>PVC: data-web-2<br/>DNS: web-2.nginx.ns.svc.cluster.local"] PV0[(PV data-web-0<br/>retained on Pod delete)] PV1[(PV data-web-1)] PV2[(PV data-web-2)] SS -- "creates in order:<br/>0 → 1 → 2" --> POD0 SS --> POD1 SS --> POD2 SS -- "binds via<br/>volumeClaimTemplates" --> PV0 SS --> PV1 SS --> PV2 POD0 -. "PVC reused across restarts" .-> PV0 POD1 -.-> PV1 POD2 -.-> PV2 HS -- "DNS A records per Pod" --> POD0 HS --> POD1 HS --> POD2 POD1 -. "peer discovery via DNS<br/>web-0.nginx... web-2.nginx..." .-> POD0 POD2 -.-> POD0 POD2 -.-> POD1
What this diagram shows. A StatefulSet named web with replicas: 3 and serviceName: nginx produces three Pods named web-0, web-1, web-2 — created strictly in that order, each waiting for the previous to be Running and Ready before the next is created (the default OrderedReady policy). Each Pod has a dedicated PersistentVolumeClaim minted from the StatefulSet’s volumeClaimTemplates, with predictable PVC names like data-web-0 that survive Pod deletion: when web-1 is rescheduled or restarted, the new Pod gets the same PVC bound to the same underlying PV with the same data. The companion headless Service nginx (with clusterIP: None) instructs CoreDNS to expose per-Pod DNS records of the form <pod-name>.<service-name>.<namespace>.svc.cluster.local so peer Pods can discover each other by name — the foundation for clustered apps that need to establish quorum, elect a leader, or shard data based on stable membership. The insight to extract is that every part of a Pod’s identity that matters for distributed-systems coordination — name, DNS name, storage — is stable across restarts. This is what Deployments cannot give you, and what makes StatefulSet the right primitive for databases.
Mechanical Walk-through
The four guarantees
The StatefulSet contract, as enumerated in the documentation (kubernetes.io):
- Stable, unique network identifiers. Each Pod’s hostname is
<statefulset-name>-<ordinal>(web-0,web-1, …). The companion headless Service publishes DNS A records keyed on this name so peers can resolve each other. The hostname survives Pod restarts and reschedules — a newweb-1Pod after the old one died has the same name. - Stable, persistent storage. Each Pod gets a PVC instantiated from
spec.volumeClaimTemplates, with name<template-name>-<statefulset-name>-<ordinal>(e.g.,data-web-0). The PVC is not deleted when the Pod is deleted, scaled down, or the StatefulSet itself is deleted — so a rescheduled Pod re-attaches to the same data. - Ordered, graceful deployment and scaling. Pods are created/scaled-up in order
0, 1, 2, …, N-1, each only after the previous is Running and Ready. Scale-down proceeds in reverse:N-1, N-2, …, 0, each only after the previous has fully terminated. This is theOrderedReadypolicy (default);Parallelpolicy lifts the ordering for both creation and deletion (but still preserves the identity guarantees). - Ordered, automated rolling updates. On
spec.templatechange, Pods are replaced in reverse ordinal order (N-1, N-2, …, 0), one at a time, each waiting for the previous replacement to be Ready before proceeding. Thepartitionfield allows phased rollouts (canary by ordinal). TheOnDeletestrategy turns this off — replacements happen only when the user manually deletes a Pod.
The companion headless Service
A StatefulSet requires (by convention; not enforced by the API server) a Headless Service referenced via spec.serviceName. A headless Service is a Service with spec.clusterIP: None — it gets no virtual IP, but CoreDNS publishes per-Pod A records for it. With a normal Service, nginx.ns.svc.cluster.local resolves to a single virtual IP that load-balances over Pods; with a headless Service, it resolves to the set of all Pod IPs, and additionally <pod-name>.<service-name>.<namespace>.svc.cluster.local resolves to the specific Pod’s IP. This is what gives peers stable, name-based addressing — see Kubernetes DNS Specification.
The most common operational mistake is forgetting to create the headless Service. The StatefulSet starts, Pods get ordinal names, but DNS doesn’t resolve them — and any clustering protocol that relies on peer addresses by name fails to bootstrap. Diagnose with nslookup web-0.nginx.default.svc.cluster.local from inside a Pod.
Pod identity is immutable in a way Pod IPs aren’t
The Pod IP changes on restart (Pod IP is allocated by the CNI plugin at Pod start; see Pod Networking). The Pod hostname does not — kubelet sets the Pod’s hostname to <statefulset-name>-<ordinal> at start. The headless Service’s DNS records are updated to point at the new Pod IP. So:
- Cassandra Pod
cass-0restarts; gets a new IP10.244.0.42 → 10.244.0.51. - DNS
cass-0.cassandra.cass.svc.cluster.localnow resolves to10.244.0.51. - Peer Pod
cass-1re-resolves the name; reconnects.
This is the magic that makes clustered apps survive on Kubernetes: the names are the stable address surface, not the IPs.
PVC lifecycle
Each volumeClaimTemplate becomes one PVC per ordinal. When the StatefulSet is scaled from 3 to 5, two new PVCs are created (data-web-3, data-web-4). When scaled from 5 back to 3, the two extra PVCs are not deleted by default — they remain bound to their PVs, holding the data, ready for a future scale-up to reuse them.
The PVC-retention-on-scale-down behavior is configurable via spec.persistentVolumeClaimRetentionPolicy (with whenDeleted and whenScaled fields, each accepting Retain or Delete). This is the StatefulSetAutoDeletePVC feature: it was alpha in v1.23 (k8s.io blog, Dec 2021), beta in v1.27 (k8s.io blog, May 2023), and graduated to GA in v1.32 — the feature gate now shows Stage: GA, Since: 1.32, Default: true (feature-gates reference, verified 2026-05-22; corroborated by the KEP-1847 implementation history: “1.23, alpha … 1.27, graduation to beta … 1.32, graduation to GA”). Because it is GA-and-default-on, the persistentVolumeClaimRetentionPolicy field is honored on any cluster from v1.32 onward without enabling anything.
The default behavior is retain on all delete and scale-down events (both fields default to Retain), which is the safe choice for stateful data and matches pre-feature behavior. The trade-off: PVCs (and underlying PVs, depending on the StorageClass’s reclaimPolicy) accumulate over the life of the cluster. Backups, snapshots, and explicit cleanup are the user’s responsibility — see Volume Snapshot, Velero.
Pod management policies
Two options (kubernetes.io — pod-management-policies):
OrderedReady(default). Create0→ wait for Ready → create1→ wait for Ready → … On scale-down, terminateN-1→ wait for full termination → terminateN-2→ … This is the conservative choice for cluster bootstrapping where each node must register/sync with previous nodes (Zookeeper, Cassandra, Galera MySQL).Parallel. Create all Pods concurrently; delete all concurrently. The ordinal-name and PVC-template guarantees are still in force, but the sequencing guarantee is dropped. Useful for apps that have their own clustering bootstrap that doesn’t depend on K8s-level ordering — e.g., a sharded Postgres where each shard is independent.
podManagementPolicy is immutable after creation. Switching requires deleting the StatefulSet (without cascade-deleting Pods, via --cascade=orphan) and recreating.
Update strategies
Two options (kubernetes.io — update-strategies):
-
RollingUpdate(default). Onspec.templatechange, Pods are replaced in reverse ordinal order (N-1, N-2, …, 0), one at a time, each replacement waiting for the previous to be Ready (and, since K8s 1.25 stable, additionally forminReadySecondsto elapse — see Kubernetes 1.25 blog post). The reverse order is deliberate: in many distributed apps, the lowest-ordinal Pod is the “primary” or “seed” — updating it last reduces total disruption.The
partitionfield gates which Pods get updated:partition: 2means only Pods with ordinal≥ 2get the new template; ordinals0and1keep the old template untilpartitionis decreased. This is the native canary mechanism for StatefulSets — bump the partition down one Pod at a time, observe, proceed. -
OnDelete. The StatefulSet controller does not update Pods on template change. Users must manuallykubectl delete pod web-2to have it recreated with the new template. This is the escape hatch for apps where the right rolling-update order is application-specific (e.g., update the leader last, after demoting it) — a custom operator or manual procedure can drive the deletion order.
MaxUnavailable for StatefulSet rolling updates
spec.updateStrategy.rollingUpdate.maxUnavailable is gated by the MaxUnavailableStatefulSet feature gate (KEP-961). It was introduced as Alpha (off by default) in v1.24, where it sat for ten release cycles, and was finally promoted to Beta (on by default) in v1.35 — the feature-gates reference shows the rows MaxUnavailableStatefulSet | false | Alpha | 1.24 | 1.34 followed by MaxUnavailableStatefulSet | true | Beta | 1.35 (feature-gates reference, verified 2026-05-22). So on a cluster older than v1.35 you must explicitly enable the gate on both the kube-apiserver and the kube-controller-manager (a skew between them either silently ignores the field or rejects it at admission); on v1.35+ the field is accepted by default, though the default value of maxUnavailable remains 1, so behavior is unchanged until you raise it.
maxUnavailable allows a StatefulSet rolling update to replace multiple Pods at once (subject to the bound), at the cost of giving up the strict reverse-ordinal-one-at-a-time guarantee. Most production users do not need this — the whole point of StatefulSet is conservative serial rollouts. Choose this only when you have empirically measured that your app tolerates parallel restarts.
minReadySeconds (stable in 1.25)
The Kubernetes 1.25 release graduated spec.minReadySeconds for StatefulSet to stable (kubernetes.io blog). The semantics match the Deployment field: a Pod must be Ready and minReadySeconds must have elapsed since it became Ready before the controller counts it Available and proceeds with the next Pod in the rollout. This is critical for stateful apps that require post-Ready warmup (cache hydration, secondary index build, log replay) before being safe to put under load.
Configuration / API Surface
A StatefulSet running a Postgres-flavored peer-discoverable cluster, with line-by-line commentary:
apiVersion: v1
kind: Service # the REQUIRED companion headless service
metadata:
name: postgres
namespace: db
spec:
clusterIP: None # 'None' = headless; CoreDNS publishes per-Pod A records
selector:
app: postgres
ports:
- port: 5432
name: pg
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: db
spec:
serviceName: postgres # MUST match the headless Service name
replicas: 3 # postgres-0 (primary), postgres-1, postgres-2 (replicas)
podManagementPolicy: OrderedReady # default; switch to Parallel if your app prefers
minReadySeconds: 30 # wait 30s after Ready before counting Available
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 0 # 0 = update all Pods; set higher for canary by ordinal
selector:
matchLabels:
app: postgres # MUST match template labels; immutable
template:
metadata:
labels:
app: postgres
spec:
terminationGracePeriodSeconds: 60 # databases want time to flush
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
name: pg
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: pg-secret
key: password
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name # 'postgres-0' etc — app uses this for replication setup
readinessProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
initialDelaySeconds: 10
periodSeconds: 5
volumeMounts:
- name: data # MUST match a volumeClaimTemplate name below
mountPath: /var/lib/postgresql/data
volumeClaimTemplates: # per-Pod PVCs created from this template
- metadata:
name: data # PVCs named: data-postgres-0, data-postgres-1, ...
spec:
accessModes: [ReadWriteOnce]
storageClassName: gp3-encrypted # the CSI driver behind this provisions actual PV
resources:
requests:
storage: 100Gi
persistentVolumeClaimRetentionPolicy: # PVC lifecycle on scale-down / StatefulSet deletion
whenDeleted: Retain # don't auto-delete PVCs when StatefulSet is deleted
whenScaled: Retain # don't auto-delete PVCs on scale-downKey surface points:
serviceNameis mandatory but not validated. The API server accepts a StatefulSet with aserviceNamereferring to a nonexistent Service. The StatefulSet starts; Pods are created; but DNS lookups fail. Always create the Service before (or together with) the StatefulSet.volumeClaimTemplatesis append-only-ish. Adding new templates works; modifying existing ones is rejected; removing them strands PVCs. Plan carefully.- The DNS pattern is fixed.
<pod-name>.<service-name>.<namespace>.svc.cluster.localis wired into CoreDNS. The full FQDN — and the shorter<pod-name>.<service-name>if the resolver is in the same namespace — is what cluster apps should use as their peer addresses. apps.kubernetes.io/pod-indexis automatically added as a label on each StatefulSet Pod, exposing the ordinal so operators can select Pods by index without parsing the name. This is thePodIndexLabelfeature — alpha in v1.28, stable (gate locked on) in v1.31 (feature-gates reference, verified 2026-05-22).- The
startordinal field (spec.ordinals.start) reached stable in v1.31 (per the StatefulSet concept page,FEATURE STATE: Kubernetes v1.31 [stable]); it lets ordinal numbering start from a value other than 0 — useful for migrations where you want to start fresh frommyapp-7without renaming. Pods are then assigned ordinals from.spec.ordinals.startthrough.spec.ordinals.start + .spec.replicas - 1.
Failure Modes
-
Missing headless Service. The most common StatefulSet error. Pods come up with correct names, but
nslookup web-0.nginx.default.svc.cluster.localreturnsNXDOMAIN. Apps fail to bootstrap because peer discovery doesn’t resolve. Diagnose:kubectl get svc nginx; ensure it exists withclusterIP: None. -
Stuck rollout because Pod
N-1never becomes Ready. Reverse-ordinal rollout means the highest-ordinal Pod is updated first. If it never becomes Ready, the rollout stalls there forever. Common causes: a new image with a bug; a new readiness probe that’s stricter; a missing dependency. Diagnose:kubectl get pods; the highest-ordinal Pod will be inPending/CrashLoopBackOff. Either fix the template and let it retry, or setupdateStrategy.partitionhigh enough to skip the broken Pod while you investigate. -
OrderedReady deadlock at scale-down. If Pod
web-1is stuck (not terminating cleanly, perhaps because a finalizer is blocking it or a volume detach is hung),web-0will not be deleted on scale-down to 0 — the controller is waiting forweb-1to fully terminate first. Diagnose:kubectl get podsshows web-1 in Terminating; check finalizers and volume status. Force-delete withkubectl delete pod web-1 --force --grace-period=0if necessary, accepting the data-corruption risk. -
PVC and PV out of sync after deletion. Deleting a StatefulSet (without
--cascade=orphan) deletes the Pods but the defaultwhenDeleted: Retainretains PVCs. If you then recreate the StatefulSet with the same name, the new Pods adopt the existing PVCs — the data is preserved. But if the new PodSpec mounts a different path or expects a different schema, you get silent inconsistencies. The defensive operator: explicitlykubectl delete pvc -l app=postgresafter deleting the StatefulSet, when you actually want fresh storage. -
OrderedReadypaired with slow-starting apps. A 100-replica Cassandra cluster withOrderedReadyand 90-secondminReadySecondstakes at least 150 minutes to deploy from scratch. Often a misjudgment — useParallelfor apps whose bootstrap is genuinely independent of ordering. -
Forgetting that
spec.replicaschange is not a rolling update. Scaling a StatefulSet from 3 to 5 createsweb-3andweb-4with the current template; scaling back to 3 deletes them. Template changes are what trigger rolling updates. A user expecting “rolling update + scale” in one apply will get only the scale; the template change needs a subsequent rollout. -
Headless Service selector mismatch. If the Service’s selector doesn’t match the StatefulSet’s Pod labels, no DNS records are published. Pods are running with stable names but unreachable by name. Diagnose:
kubectl get endpoints <service>shows empty endpoints despite Pods running. Fix the selector. -
hostNetwork: truein a StatefulSet. Combining hostNetwork with StatefulSet hostname semantics is fragile — the Pod’s hostname is set to<sts>-<ordinal>but the system hostname on the node (which hostNetwork Pods inherit) may not match. Some apps that hitgethostname(3)directly get the wrong name. Avoid unless you specifically know your app handles it. -
CSI driver doesn’t support
volumeClaimTemplateszone-pinning. If your StorageClass provisions zonal disks (AWS EBS, GCP PD), the first PV fordata-web-0is created in some zone (e.g., us-east-1a). If the Pod is later rescheduled to a node in us-east-1b, the disk can’t attach — RWO disks are zonal. Symptom:web-0stuck inContainerCreatingwithFailedAttachVolume. Fix: use a zonal node affinity, or a regional StorageClass (regional PD on GCP), or topology-aware provisioning (WaitForFirstConsumer binding mode on the StorageClass). -
Quorum-loss during rolling update with too-low replica count. A 3-Pod etcd or Zookeeper cluster: rolling update kills
etcd-2, the quorum is(2 of 2 remaining alive). Ifetcd-2doesn’t come back Ready quickly, andetcd-1is now next to be updated, killingetcd-1drops to(1 of 1 remaining)— quorum lost. Stock RollingUpdate doesn’t model this; you need either custom operators (Zalando Patroni operator, etcd-operator) or carefulpartitionmanagement. This is the most common reason “we lost the cluster during a Kubernetes update.”
Alternatives and When to Choose Them
- Deployment — the default for everything that doesn’t need identity. If a workload’s Pods are interchangeable, use Deployment. Even if you mount a persistent volume for cache, Deployment + a single shared PVC (RWX) is usually preferable to StatefulSet.
- Database operator with custom CRD (CNPG, Vitess, Zalando Postgres-Operator) — for serious production database use, the StatefulSet alone is rarely enough. An Operator Pattern CRD wraps the StatefulSet with failover orchestration, backups, point-in-time recovery, and topology-aware rolling updates that respect application-level invariants (don’t update the primary while a backup is running, demote leaders before replacing them). See Database Operators.
- Managed database services (RDS, Cloud SQL, MongoDB Atlas) — for many teams, the right answer is “don’t run the database on Kubernetes at all.” StatefulSet exists and works; managed services exist and work better operationally. Choose K8s-hosted databases only when you have operator expertise, regulatory requirements, or a specific cost driver.
- Job with persistent volume — for batch processing of stateful data that’s run-to-completion rather than long-running. Different semantic profile.
- DaemonSet with persistent volume — if the storage is genuinely per-node (e.g., a local-storage cache, a per-node telemetry buffer), DaemonSet plus
hostPathor a local PV is the right shape, not StatefulSet.
Production Notes
- Confluent’s Kafka-on-Kubernetes deployment guide uses a StatefulSet per Kafka broker pool, with
serviceNameset to a headlesskafka-brokersservice. Brokers discover each other via<broker-name>.kafka-brokers.<ns>.svc.cluster.local. Confluent’s blog explicitly warns against using aclusterIPService for Kafka, since the load balancing would route producer/consumer traffic to random brokers — disastrous for partition assignment. - CloudNativePG (CNPG) for Postgres layers a
ClusterCRD on top of a StatefulSet, but with a critical departure: CNPG uses a Deployment for each instance for finer control over rolling updates and avoids StatefulSet’s reverse-ordinal-only update order. This is a recurring theme in mature operators — the StatefulSet semantics are too rigid for orderly failover, so operators replicate parts of the StatefulSet behavior in their own controllers. - Cassandra-on-K8s (the official cass-operator) uses StatefulSet directly, with a custom rolling-update procedure that pauses between Pod restarts to wait for the Cassandra cluster’s own gossip-driven Ready state — going beyond just K8s readiness probes.
- k8s.af incidents involving StatefulSets typically cluster around: (a) PVC retention misunderstandings (deleting a StatefulSet, losing the data); (b) zone-pinning of EBS volumes blocking reschedules; (c)
OrderedReadydeadlocks during outages where Pod N-1 is stuck and the whole cluster can’t be scaled or updated. - Spotify’s general guidance internally is that StatefulSets are a “platform-team-approved-only” workload type — application teams default to Deployment + managed cloud database. The operational overhead of StatefulSet is real and ongoing; not every team needs to learn it.
See Also
- Pod — what the StatefulSet manages
- Headless Service — the required companion Service
- CoreDNS / Kubernetes DNS Specification — the DNS layer publishing per-Pod records
- PersistentVolume / PersistentVolumeClaim / StorageClass — the storage layer behind
volumeClaimTemplates - Volume Snapshot — the backup primitive for StatefulSet PVCs
- Container Storage Interface — what dynamically provisions the PVs
- Stateful Workloads on Kubernetes — broader pattern note
- Database Operators — the operator wrappers around StatefulSet
- Operator Pattern — the extension model these operators use
- Deployment — the stateless sibling
- DaemonSet / Job — the other workload primitives
- Pod Disruption Budget — how to bound voluntary disruption during node drains
- Kubernetes Control Loop Pattern — the reconciliation model the StatefulSet controller is an instance of
- Kubernetes MOC — umbrella index