Kubernetes · commonly confused
Deployment vs StatefulSet
Short answer
First published
On this page · 10 sections
In simple terms
Both run several copies of the same pod and replace them when they fail. The difference is whether the copies are interchangeable. A Deployment treats them as identical and disposable: when one dies, a new one with a new name takes its place, and nothing remembers the old one. A StatefulSet gives each copy a fixed identity — web-0, web-1, web-2 — and keeps that identity attached to it through restarts and moves between nodes.
That identity comes with two things a Deployment does not give you. Each pod can get its own persistent volume, from the StatefulSet's volume claim templates, and when the pod is rescheduled it reconnects to that same volume rather than starting empty. And by default the pods start, and on scale-down stop, in order: web-1 waits for web-0 to be running and ready before it is created, which matters for clustered databases where members have to join one at a time.
A stateless application wants a Deployment. A web service, an API, a worker reading from a queue — none of these care which replica handles a request, and the ordering and fixed names of a StatefulSet would only slow down rollouts. The documentation says this directly: if an application does not require stable identifiers or ordered deployment, deletion or scaling, use a workload object that provides stateless replicas.
StatefulSets are for the things that remember: databases, message brokers, consensus systems, anything where a member must find the same data and be reachable at the same address after a restart. They also come with responsibilities a Deployment never asks of you, and one that catches people out is about cleanup — deleting or scaling down a StatefulSet does not delete its volumes.
Words used on this page
- ReplicaSet
- Keeps a set number of identical Pods running. A Deployment creates one per version of its Pod template, and Pods take their names from it plus a random suffix.
- ordinal
- The number at the end of a StatefulSet Pod's name, such as the 0 in
web-0. Pods get 0 up to replicas minus 1 by default. - headless Service
- A Service with
clusterIP: None. Instead of one shared virtual IP, it gives each Pod behind it its own DNS name. - PersistentVolumeClaim (PVC)
- A request for storage of a given size and access mode. It binds to a PersistentVolume, and Pods mount the claim.
- PersistentVolume (PV)
- A piece of storage in the cluster, either created by an administrator or provisioned on demand from a StorageClass.
- volumeClaimTemplates
- The part of a StatefulSet that creates one claim per Pod, named after the template and the Pod, such as
www-web-0. - StorageClass
- A kind of storage and how to provision it. Claims that name no class use the one marked default.
- reclaim policy
- What happens to a PersistentVolume when its claim is deleted.
Deleteremoves the volume and the storage behind it;Retainkeeps both for an administrator to clean up. - podManagementPolicy
- How a StatefulSet creates and removes Pods:
OrderedReady, one at a time in order (the default), orParallel, all at once. - persistentVolumeClaimRetentionPolicy
- StatefulSet setting that decides whether claims are kept or deleted when the StatefulSet is deleted (
whenDeleted) or scaled down (whenScaled). Both default toRetain.
What actually happens
The documentation lists what a StatefulSet provides: stable, unique network identifiers; stable, persistent storage; ordered, graceful deployment and scaling; and ordered, automated rolling updates — where stable means persistent across pod (re)scheduling. Each pod's hostname is the StatefulSet name plus its ordinal, and a headless Service gives each one a DNS name under its own domain. The documentation says StatefulSets require that headless Service for their pods' network identity, and that creating it is your responsibility. The API does not check: a StatefulSet whose Service is missing is accepted, and its pods' DNS names do not resolve until the Service exists.
Storage is per pod. For each entry in volumeClaimTemplates, each pod receives its own PersistentVolumeClaim, and when a pod is rescheduled onto a node its volume mounts reattach the volumes behind its own claims. By default those volumes are deliberately not deleted when the pods or the StatefulSet are deleted, or when it is scaled down; the documentation says this is for data safety. The persistentVolumeClaimRetentionPolicy field, stable since Kubernetes 1.32 and first available in 1.23, can delete them on deletion, on scale-down, or both.
Ordering is enforced by readiness. Under the default pod management policy, OrderedReady, each pod must be Running and Ready before its successor is created, and before a pod is terminated all of its successors must be completely shut down — so a failing readiness probe on one member halts the whole sequence. The documentation warns that rolling updates under OrderedReady can reach a broken state that requires manual intervention, and that setting terminationGracePeriodSeconds to 0 on a StatefulSet is unsafe.
A Deployment manages a ReplicaSet of interchangeable pods. The scheduler places them freely, they share no per-pod identity, and replacement pods do not inherit any predecessor's name or volume claim. That is what lets a Deployment roll out quickly and scale in any order — and why it is the wrong tool when a member's identity and data must survive its replacement.
The parts a StatefulSet adds
# Required: a headless Service. You create it; the StatefulSet does not.
apiVersion: v1
kind: Service
metadata:
name: db
spec:
clusterIP: None # headless — gives each pod its own DNS name
selector:
app: db
ports:
- port: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db # ties pod DNS names to the Service above
replicas: 3 # pods are db-0, db-1, db-2 — in that order
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: <database-image>
volumeClaimTemplates: # one claim PER POD, reattached on reschedule
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
# After scaling down or deleting, the claims are still here — by design:
# kubectl get pvc -l app=dbThe image is a placeholder. Each pod gets a claim named after the template and the pod, such as data-db-0, which is why a recreated db-0 finds its old data rather than an empty volume.
Try it yourself
Before you start
- A cluster you can practise in, ideally one with no real workloads, and
kubectlconfigured for it. The Kubernetes tutorial used below works in thedefaultnamespace. - Dynamic volume provisioning with a default StorageClass:
kubectl get storageclassshould show one marked(default). Without it, the tutorial says to create two 1 GiB PersistentVolumes by hand before you start. - Permission to create StatefulSets, Services and PersistentVolumeClaims, and to run
kubectl execin Pods.kubectl auth can-i create statefulsets.appsshould printyes. - Two terminal windows: one to keep
kubectl get pods --watch -l app=nginxrunning, one for the commands. - A plan for cleaning up. Deleting the StatefulSet leaves its claims behind, and the tutorial ends by deleting them by name. With a
Deletereclaim policy that also destroys the volumes, so only do it in a test cluster. The example below is quoted from the Kubernetes v1.37 documentation, not run here.
Pods come back with their own data, and claims outlive a scale-down
Quoted from the Kubernetes tutorial StatefulSet Basics; not reproduced here, because no cluster was available. The StatefulSet web runs nginx behind the headless Service nginx, with one 1 GiB claim per Pod from the template www, and each Pod has written its own hostname into the index.html on its volume. Between the steps shown, the tutorial waits for both Pods to return after the delete, and scales the StatefulSet up to 5 replicas before the patch. The claim listing is labelled as similar to what you will see.
Command
kubectl delete pod -l app=nginx
for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
kubectl patch sts web -p '{"spec":{"replicas":3}}'
kubectl get pvc -l app=nginxOutput · as shown in Kubernetes: StatefulSet Basics
$ kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted
$ for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
web-0
web-1
$ kubectl patch sts web -p '{"spec":{"replicas":3}}'
statefulset.apps/web patched
$ kubectl get pvc -l app=nginx
NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
www-web-0 Bound pvc-15c268c7-b507-11e6-932f-42010a800002 1Gi RWO 13h
www-web-1 Bound pvc-15c79307-b507-11e6-932f-42010a800002 1Gi RWO 13h
www-web-2 Bound pvc-e1125b27-b508-11e6-932f-42010a800002 1Gi RWO 13h
www-web-3 Bound pvc-e1176df6-b508-11e6-932f-42010a800002 1Gi RWO 13h
www-web-4 Bound pvc-e11bb5f8-b508-11e6-932f-42010a800002 1Gi RWO 13hHow to read it
- Both Pods were deleted, and the StatefulSet recreated them under the same names,
web-0andweb-1. A Deployment would have replaced them with Pods whose names end in a new random suffix. curlstill returnsweb-0andweb-1: each new Pod mounted its own claim again, so the file it wrote before the delete was still there. The tutorial notes this holds whichever node the Pod lands on.- The
patchset replicas to 3. The tutorial's watch showsweb-4terminating first and thenweb-3: highest ordinal first, one at a time. kubectl get pvcstill lists five claims, allBound.www-web-3andwww-web-4outlived their Pods, which is the defaultRetainbehaviour. Scale back up to 5 and the newweb-3andweb-4mount these same claims and find the old data.- Each claim is named template plus Pod (
wwwandweb-0givewww-web-0) and carries theapp=nginxlabel, which is why-l app=nginxstill finds claims whose Pods are gone.
The mistake this causes
Assuming that scaling down or deleting a StatefulSet cleans up its storage, the way removing a Deployment leaves nothing behind.
Why people do it Deployments with ordinary pod storage really do leave nothing to clean up, so the habit carries over. StatefulSets do the opposite on purpose: by default their volumes are not deleted on scale-down or deletion, for data safety, unless persistentVolumeClaimRetentionPolicy says otherwise.
What you see PersistentVolumeClaims and their volumes outlive the workload — storage that keeps being provisioned and, on a cloud provider, billed for nothing. And scaling back up later reattaches the old data to the pod with the same ordinal, which is a surprise if you expected a fresh start.
If it goes wrong
web-0staysPendingwithpod has unbound immediate PersistentVolumeClaims, andkubectl get pvcshows its claim asPendingtoo- Nothing can provision the claim. Check
kubectl get storageclassfor one marked(default), or name a class instorageClassNamein the claim template, or create matching PersistentVolumes by hand. The claim template cannot be edited later, so fix this before you rely on the StatefulSet. web-1never appears, andweb-0shows0/1under READY- With the default
OrderedReadypolicy,web-1waits untilweb-0is Running and Ready.kubectl describe pod web-0shows why it is not ready, for example a failing readiness probe. If your members do not need ordering,podManagementPolicy: Parallelstarts them together, but the field cannot be changed on an existing StatefulSet. - A rolling update stops at one Pod that never becomes Ready, and reverting the template changes nothing
- This is the known issue the StatefulSet page describes under forced rollback. Revert the template, then delete the Pods that were already started with the bad configuration; the controller recreates them from the reverted template.
- Editing the storage size in
volumeClaimTemplatesis rejected:spec.volumeClaimTemplates...field is immutable - The claim template is fixed once the StatefulSet exists. To grow existing volumes, edit each PersistentVolumeClaim's request instead; that only works if its StorageClass has
allowVolumeExpansion: true. nslookup web-0.nginxfails- Check that a Service with the name in the StatefulSet's
serviceNameexists in the same namespace and hasclusterIP: None. If the Pod has only just started, DNS may be caching an earlier failed lookup; the StatefulSet page notes that CoreDNS caches for 30 seconds.
How it shows up in production
A team tries out a clustered database as a StatefulSet in a test namespace, scales it from five members to one, and later deletes it. The cluster looks clean in kubectl get pods.
The cloud storage bill does not drop. Every claim from the volumeClaimTemplate is still there — one per pod that ever existed — each holding a provisioned disk.
Weeks later, someone recreates the StatefulSet with the same name in the same namespace to test again. Pod db-0 starts with the previous test's data already on its volume, because its claim was never removed and a recreated db-0 reattaches to it.
Both surprises are the same documented behaviour: StatefulSet storage is kept for data safety, and removing it is a deliberate, manual step.
How to tell which one you are hitting
- If a replica is replaced, must the new one have the same name and the same data?
- If yes, StatefulSet. If any replica can serve any request and start empty, Deployment — and the documentation recommends that whenever stable identity is not needed.
- Do members have to start, stop or upgrade in a particular order?
- Ordered start-up and shutdown is a StatefulSet feature, enforced through readiness. If order does not matter, a Deployment rolls out faster and scales freely.
- Does each replica need its own storage, rather than none or a shared volume?
- Per-replica persistent volumes come from volumeClaimTemplates, which only StatefulSets have.
- Is someone ready to own the extra operations?
- A headless Service to create, volumes to clean up by hand unless you set a retention policy, and rollouts that can stall on one unready member. If those costs buy nothing for this workload, it should be a Deployment.
Security considerations
- Claims left after a scale-down or delete still hold the data, and any Pod in the namespace can mount a claim by name. Anyone allowed to create Pods there can read what a removed member left behind, so delete claims you no longer need, or set
persistentVolumeClaimRetentionPolicyso they go with their Pods. - A StatefulSet recreated with the same name in the same namespace picks up the old claims and their data. Never reuse a StatefulSet name for a different application or tenant without deleting its claims first.
- Deleting a claim does not always delete the data. Under a
Retainreclaim policy the volume becomesReleasedwith the previous data still on it until an administrator removes it; underDeletethe storage itself is destroyed and cannot be recovered. Check the policy withkubectl get pvbefore cleaning up. - Do not force-delete StatefulSet Pods with
--grace-period=0 --force, and do not setterminationGracePeriodSeconds: 0. Force deletion frees the Pod's name at once, so a replacement can start while the old Pod is still running; the Kubernetes docs warn this breaks the one-Pod-per-identity guarantee and can lead to split brain and data loss.
In an interview
How this gets asked in an interviewpreparation
Usually phrased as When would you use a StatefulSet instead of a Deployment?
What a strong answer contains When replicas are not interchangeable: they need stable network identities, their own persistent storage that follows them across rescheduling, or ordered deployment and scaling — databases, brokers, consensus systems. A strong answer adds the costs: StatefulSets require a headless Service you create yourself, their volumes are not deleted on scale-down or deletion, and under the default OrderedReady policy a rollout can stall behind one unready pod. And it says the default should be a Deployment unless one of those properties is genuinely needed.
The follow-up "You deleted a StatefulSet and recreated it, and pod 0 has old data. Why?" Its PersistentVolumeClaim was never deleted — StatefulSet volumes are kept for data safety — and the recreated pod with the same ordinal reattached to it.
Sources
The behaviour this page describes, as documented by the projects themselves.
Did this get you to an answer?
No text box on purpose — please do not paste production logs anywhere
Something wrong or out of date? Report it