KubernetesXVII · StatefulSetsStatefulSets
StatefulSet anti-patterns — when not to reach for the database controller
What you'll learn
- Identify workloads where StatefulSets are the wrong controller
- Reason about the operational cost of StatefulSets vs Deployments
- Recognise the "StatefulSet as a habit" anti-pattern
- Apply the right controller for each workload class
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
StatefulSets are sometimes treated as the “advanced Deployment.” They are not. They are a controller with a narrow purpose — clustered stateful workloads with stable identity and ordered lifecycle. Used outside that purpose, they impose operational cost without benefit, and the hidden cost shows up at delete time, scale-down time, and every storage event. This lesson is the case for using the right controller for the workload.
Anti-pattern 1: StatefulSet for a stateless service
The most common mistake. A team adopts Kubernetes and notices that StatefulSets have per-Pod PVCs. They generalise this as “StatefulSets give me persistent storage” and apply it to a stateless web service:
# WRONG: stateless web with StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: web-h
replicas: 6
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
template:
spec:
containers:
- name: web
image: nginx:1.27.2
volumeMounts:
- name: data
mountPath: /var/www/html
The web service has no business with per-Pod PVCs. The
“data” is the image, served from /usr/share/nginx/html,
not from /var/www/html. The PVCs cost 6 × 10Gi of
persistent storage for no reason. Scaling requires ordered
operations that a stateless Deployment does not need.
The right answer:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 6
selector:
matchLabels:
app: web
template:
spec:
containers:
- name: web
image: nginx:1.27.2
Stateless services use Deployments. No PVCs. No headless Service. No ordered scaling. The cluster autoscaler can move Pods freely; the rollout uses RollingUpdate; the cost is the cost of memory + CPU, not the cost of 6 × 10Gi of storage.
Anti-pattern 2: single-replica StatefulSet
A second common mistake: a team has a database workload and decides “it’s stateful, so StatefulSet.” With a single replica, there is no ordered lifecycle benefit (only one Pod), no per-Pod PVC advantage over a Deployment with a single PVC, and no clustering benefit (no quorum).
# WRONG: single-replica "database"
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db-h
replicas: 1
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Gi
template:
spec:
containers:
- name: postgres
image: postgres:16
The single replica gives up the StatefulSet’s main benefits (clustering) and keeps all the costs (headless Service, ordered lifecycle, PVC complexity).
The right answer: if the database has no replicas, treat it as a single-pod workload with backup policy and a clear recovery path. A Deployment with a PVC is simpler:
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
spec:
replicas: 1
selector:
matchLabels:
app: db
template:
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: db-data
If clustering is desired, deploy a 3-replica (or 5-replica) StatefulSet with proper quorum and bootstrap logic. A single-replica “stateful” workload is rarely the right design.
Anti-pattern 3: StatefulSet for ephemeral workloads
A workload that “needs persistence for the lifetime of the Pod” but the data is gone when the Pod terminates:
# WRONG: ephemeral build cache
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: build-cache
spec:
serviceName: build-cache-h
replicas: 3
volumeClaimTemplates:
- metadata:
name: cache
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 50Gi
The “persistence” lasts only as long as the Pod. When the Pod is rescheduled, the new Pod gets a new PVC, mounts it, and the old data is orphaned.
The right answer: emptyDir for Pod-local ephemeral
storage, or a node-local cache (e.g., a local PV with
ReadWriteOnce on a specific node, or an external cache
service). StatefulSets for “data we don’t care about” are
expensive.
Anti-pattern 4: StatefulSet without ordered bootstrap
A workload that does not actually need ordered lifecycle but is wrapped in a StatefulSet “to be safe.” The team deploys three replicas simultaneously because the application’s bootstrap is fast and does not depend on order. The StatefulSet’s ordering forces sequential rollout; the team complains about slow deployments.
The right answer: a Deployment. StatefulSet’s ordering is a correctness feature, not a safety feature for applications that don’t need it.
Anti-pattern 5: StatefulSet for a workload that should be a Job
A batch workload that runs once per replica and exits. A StatefulSet keeps the Pods running; the operator notices high resource usage from “idle” Pods.
# WRONG: batch workload
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: batch
spec:
serviceName: batch-h
replicas: 3
template:
spec:
containers:
- name: batch
image: batch-runner:1
The right answer: a Job with completions: 3 and
parallelism: 3. The Job runs the workload, completes, and
the cluster reclaims the resources.
Anti-pattern 6: StatefulSet with a hidden Operator
Some Operators manage the StatefulSet under the hood (e.g.,
the PostgreSQL Operator, the Kafka Operator). The
StatefulSet is a building block; the Operator owns its
lifecycle. If your team has deployed a custom resource (e.g.,
PostgresCluster, KafkaCluster) and the Operator creates
a StatefulSet, do not edit the StatefulSet directly.
The right answer: interact with the Operator’s custom resource. Editing the StatefulSet bypasses the Operator’s reconciliation; the Operator may revert the change or delete the StatefulSet on its next reconcile.
The cost of a wrong StatefulSet
A wrong StatefulSet accumulates costs in six places:
flowchart LR
A[Wrong StatefulSet] --> B["Headless Service<br/>DNS records to maintain"]
A --> C["Per-Pod PVCs<br/>storage cost"]
A --> D["Ordered lifecycle<br/>slower rollouts"]
A --> E["Partition field<br/>rollout complexity"]
A --> F["StorageClass policy<br/>data-loss risk on delete"]
A --> G["Operator complexity<br/>partition, ordinal, PVC"]
For a stateless service that has no business being a StatefulSet, each of these costs is paid for no benefit.
The decision matrix
| Workload | Right controller |
|---|---|
| Stateless HTTP service | Deployment |
| Stateless batch (parallel) | Job |
| Periodic batch | CronJob |
| Stateful batch (parallel) | Job with volumeClaimTemplates? No — use a Deployment or a custom controller |
| Replicated database | StatefulSet (managed by Operator preferred) |
| Single-node database | Deployment + PVC + backup policy |
| Node-local agent | DaemonSet |
| Cluster-wide singleton | Deployment with replicas: 1 and a leader election sidecar |
Quiz
Knowledge check · 4 questions
Q1. Which workload is a candidate for a StatefulSet?
Q2. A single-replica StatefulSet is a reasonable choice for any stateful workload because the StatefulSet guarantees persistence.
Q3. Your team has a stateless nginx Deployment with replicas 6. Someone adds a volumeClaimTemplate to give each Pod a 10Gi PVC. Diagnose.
Deployment web with replicas 6 now has volumeClaimTemplates data 10Gi. Each Pod has a 10Gi PVC; cluster storage usage is 60Gi. The data is unused.
Q4. Why is the operational cost of a StatefulSet higher than a Deployment, and when does the cost pay off?
Passing score: 75%. Answers are checked in this browser.
Production discipline
- Default to Deployment. Treat StatefulSet as the exception, not the default. The decision to use StatefulSet should require a written justification in the manifest comment and the change ticket.
- If you find yourself adding
serviceNameto a manifest, ask why. A headless Service exists for one reason: to give individual Pods a stable DNS name. If the workload does not need individual Pod addressing, you do not need a headless Service, and you do not need a StatefulSet. - Audit the cluster for unnecessary StatefulSets. A
query for
kind=StatefulSet, spec.replicas <= 1is the starting point. Each match needs a justification. - Use Operators for stateful workloads. A home-grown StatefulSet is rarely the right answer for a production database. The Operator handles bootstrap, replication, failover, and backup. The team writes the application; the Operator owns the cluster.
- Test the deletion path. A wrong StatefulSet that is
deleted with
reclaimPolicy: Deletestorage loses data. Document the deletion runbook; test it in staging.
StatefulSets are not a badge of seriousness. They are a controller with a specific purpose. Operators who reach for the right controller have quieter on-call rotations.