Skip to main content
RunBook Academy

KubernetesXVII · StatefulSetsStatefulSets

StatefulSet anti-patterns — when not to reach for the database controller

Advanced⏱ ~17 minkubectlkubeadm

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

Not yet marked complete on this device.

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

WorkloadRight controller
Stateless HTTP serviceDeployment
Stateless batch (parallel)Job
Periodic batchCronJob
Stateful batch (parallel)Job with volumeClaimTemplates? No — use a Deployment or a custom controller
Replicated databaseStatefulSet (managed by Operator preferred)
Single-node databaseDeployment + PVC + backup policy
Node-local agentDaemonSet
Cluster-wide singletonDeployment with replicas: 1 and a leader election sidecar

Quiz

Knowledge check · 4 questions

  1. Q1. Which workload is a candidate for a StatefulSet?

  2. Q2. A single-replica StatefulSet is a reasonable choice for any stateful workload because the StatefulSet guarantees persistence.

  3. 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.

  4. 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 serviceName to 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 <= 1 is 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: Delete storage 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.