Kubernetes: Stateless vs Stateful

The complete beginner guide — from zero to confident

DevOps Fundamentals Step-by-Step Real Examples 17 Sections

What is "State"?

Before Kubernetes, understand one word: "State".

State = Data that an application remembers over time.

Some apps remember things (your login, shopping cart, messages). Some don't need to remember anything.

Real-Life Analogy

A calculator app — Type 2 + 2, get 4, close it. No memory needed. No state.

A bank app — Must remember your balance. Deposit $100, it MUST save that. Has state.

Two Types of Applications

Stateless Application

Does NOT save data between requests. Every request is brand new.

  • Web servers (Nginx, Apache)
  • API gateways
  • Load balancers
  • Image resizing services
  • Token validators

Stateful Application

MUST save data. Data persists between requests and restarts.

  • Databases (MySQL, PostgreSQL)
  • Message queues (Kafka, RabbitMQ)
  • Cache systems (Redis)
  • File storage systems
  • Search engines (Elasticsearch)

Real-World Analogy: Restaurant vs Hospital

Stateless = Fast Food Workers

Any worker can take your order. If Worker A is busy, Worker B helps. It doesn't matter WHO serves you. If a worker goes on break, another replaces them instantly. No worker needs to "remember" you.

Stateful = Hospital with Patient Records

Each doctor has specific patients with medical records. You can't swap Doctor A with Doctor B — Doctor B doesn't know your history! Records are critical and must not be lost.

Key Takeaway

Stateless = Workers are replaceable, no memory needed.
Stateful = Each instance has unique data that MUST be preserved.

Stateless Applications — Deep Dive

🔍 How Stateless Apps Work

👤 User Request
⚖ Load Balancer
📦 Pod 1
📦 Pod 2
📦 Pod 3
All Identical — Interchangeable
Any pod can handle any request. If Pod 2 dies, others take over.
No data is lost because no data was stored!

Stateless app: 3 identical pods behind a load balancer

Characteristics

  • No local data storage — Nothing important saved on disk
  • Identical pods — Every replica is the same
  • Interchangeable — Any pod handles any request
  • Easy to scale — Just add more pods!
  • Easy to replace — Kill a pod, start new one
  • No startup order — Start in any order

💥 When a Pod Dies

Pod 2 crashes
K8s detects it
Creates new Pod
Works immediately!

Stateful Applications — Deep Dive

🔍 How Stateful Apps Work

👤 User Request
🌐 Headless Service
🔒 mysql-0
🔒 mysql-1
🔒 mysql-2
Each has a UNIQUE name
💾 PVC-0
💾 PVC-1
💾 PVC-2
🗃 Disk 0
🗃 Disk 1
🗃 Disk 2
Each pod has a unique identity and its own dedicated disk.
If mysql-1 dies, the new mysql-1 gets the SAME disk back!

Stateful app: unique names and dedicated storage per pod

Characteristics

  • Persistent storage — Own disk that survives restarts
  • Unique identity — Predictable names: app-0, app-1, app-2
  • Ordered startup — 0 first, then 1, then 2
  • Ordered shutdown — 2 first, then 1, then 0
  • Stable network identity — Permanent DNS name per pod
  • Not interchangeable — mysql-0 ≠ mysql-1

💥 When a Pod Dies

mysql-1 crashes
K8s detects
New mysql-1
Same disk
Data preserved!

Side-by-Side Comparison

FeatureStatelessStateful
K8s ResourceDeploymentStatefulSet
Pod NamesRandom (nginx-7d9f8b-x4kl2)Predictable (mysql-0, mysql-1)
StorageNone neededOwn Persistent Volume per pod
Network IDSingle Service IPOwn DNS per pod
Scale UpAny order, instantOrdered (0→1→2)
Scale DownAny orderReverse (2→1→0)
ReplacementFresh podSame name + same disk
ComplexitySimpleComplex
ExamplesNginx, Node.js, ReactMySQL, PostgreSQL, Kafka
ServiceClusterIP / LoadBalancerHeadless (clusterIP: None)

Kubernetes Concepts You Need First

📦 Pod

Smallest unit in K8s. A "wrapper" around your container. Usually one container per pod.

📦 Pod
📡 Container
(your app: nginx)

🚀 Deployment (Stateless)

Tells K8s: "Run 3 copies of my app. If one dies, make a new one." Manages stateless apps.

🔒 StatefulSet (Stateful)

Like Deployment but with superpowers: stable names, stable storage, ordered operations.

🔗 Service

A stable "phone number" for your pods. Pods come and go, but the Service IP stays the same.

💾 PV & PVC

PV = actual disk. PVC = "I need X GB of disk." K8s matches them together.

📦 Pod says: "I need 10GB"
📄 PVC (request)
💾 PV (actual 10GB disk)
☁ Real Storage (AWS EBS, GCP Disk, NFS)

Deploying Stateless — Step by Step

Deploy Nginx web server. Stateless because it just serves pages — no data saved.

1
Create the Deployment YAML
nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment                   # Stateless!
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3                       # 3 identical copies
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80
        resources:
          requests: { memory: "64Mi", cpu: "100m" }
          limits:   { memory: "128Mi", cpu: "250m" }
2
Create Service
nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  type: ClusterIP
  selector:  { app: nginx }
  ports:
  - port: 80
    targetPort: 80
3
Deploy
$ kubectl apply -f nginx-deployment.yaml
$ kubectl apply -f nginx-service.yaml
$ kubectl get pods
NAME                                READY   STATUS    AGE
nginx-deployment-7d9f8b7945-ab12c   1/1     Running   30s
nginx-deployment-7d9f8b7945-cd34e   1/1     Running   30s
nginx-deployment-7d9f8b7945-ef56g   1/1     Running   30s
# Pod names are RANDOM!
That's it!

2 files, a few commands, done. No disks, no ordering, no special naming.

Deploying Stateful — Step by Step

Deploy MySQL. Stateful — data must survive restarts.

Why is this harder?

Need: persistent storage, stable names, ordered startup, and a headless service. 4 extra things!

1
Create Headless Service (required by StatefulSet)
mysql-headless-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql-headless
spec:
  clusterIP: None                    # THIS makes it headless
  selector:  { app: mysql }
  ports:
  - port: 3306
Headless = each pod gets its own DNS

mysql-0.mysql-headless.default.svc.cluster.local
mysql-1.mysql-headless.default.svc.cluster.local
mysql-2.mysql-headless.default.svc.cluster.local

2
Create the StatefulSet
mysql-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet                  # NOT Deployment!
metadata:
  name: mysql
spec:
  serviceName: mysql-headless        # REQUIRED
  replicas: 3
  selector:
    matchLabels:  { app: mysql }
  template:
    metadata:
      labels:  { app: mysql }
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        ports:
        - containerPort: 3306
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: "my-secret-password"
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
  # KEY DIFFERENCE: auto-create disk per pod
  volumeClaimTemplates:
  - metadata:
      name: mysql-data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: standard
      resources:
        requests:
          storage: 10Gi
3
Deploy & watch ordered startup
$ kubectl apply -f mysql-headless-svc.yaml
$ kubectl apply -f mysql-statefulset.yaml
$ kubectl get pods -w
mysql-0   1/1   Running   45s    # First
mysql-1   1/1   Running   30s    # After mysql-0 ready
mysql-2   1/1   Running   15s    # After mysql-1 ready

$ kubectl get pvc
mysql-data-mysql-0    Bound    10Gi
mysql-data-mysql-1    Bound    10Gi
mysql-data-mysql-2    Bound    10Gi
See the difference?

Stateless: 2 files, random names, no storage. Stateful: 2+ files, predictable names, storage, headless service, ordered startup.

Why Stateful is Harder — 5 Challenges

💾 1. Persistent Storage

Each pod needs its OWN disk surviving restarts. Need PV, PVC, StorageClass knowledge.

🔄 2. Ordered Operations

Start in order (0,1,2), stop in reverse (2,1,0). Master must be ready before replicas.

🌐 3. Unique Network Identity

Headless Service needed to talk to a SPECIFIC pod (e.g., the master).

💾 4. Backup & Recovery

Stateless: no backups needed. Stateful: regular backups, disaster recovery, tested restores.

🔁 5. Data Replication

Master-replica setup with init containers, sidecar scripts. StatefulSet doesn't do this for you.

Stateless
Easy
Stateful
Complex

Persistent Volumes — Deep Dive

🧩 3 Pieces of the Storage Puzzle

StorageClass What TYPE of disk?
SSD? HDD? Cloud?
💾 PersistentVolume The ACTUAL disk
10GB SSD on AWS
📄 PVC "I need a disk!"
10GB, SSD, RWO
StorageClass defines how to create → PV ← matched/bound to PVC

📄 StorageClass Example

storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp3                          # Fast SSD
reclaimPolicy: Retain               # Keep disk if PVC deleted

🔒 Access Modes

ModeShortMeaningUse Case
ReadWriteOnceRWOOne pod reads/writesDatabases
ReadOnlyManyROXMany pods readConfig files
ReadWriteManyRWXMany pods read/writeShared NFS

Headless Service — What & Why

Normal Service

Single IP. Random pod selection.

👤 Client ⚖ Service IP 🎲 Random Pod

Good for: Stateless

Headless Service

No IP. Each pod has own DNS name.

Client mysql-0.svc ★ Master
Client mysql-1.svc Replica

Good for: Stateful

Why?

Databases write to master (pod-0), read from replicas. Must target specific pods. Headless Service makes this possible.

Complete Example: Stateless Nginx

complete-nginx.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-web
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate: { maxUnavailable: 1, maxSurge: 1 }
  selector:
    matchLabels:  { app: nginx-web }
  template:
    metadata:
      labels:  { app: nginx-web }
    spec:
      containers:
      - name: nginx
        image: nginx:1.25-alpine
        ports:
        - containerPort: 80
        resources:
          requests: { cpu: "100m", memory: "64Mi" }
          limits:   { cpu: "250m", memory: "128Mi" }
        readinessProbe:
          httpGet: { path: /, port: 80 }
        livenessProbe:
          httpGet: { path: /, port: 80 }
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-web-svc
spec:
  type: LoadBalancer
  selector:  { app: nginx-web }
  ports:
  - port: 80
    targetPort: 80

Complete Example: Stateful MySQL

complete-mysql.yaml
# Secret + Headless Service + StatefulSet
---
apiVersion: v1
kind: Secret
metadata:  { name: mysql-secret }
type: Opaque
data:
  root-password: bXktc2VjcmV0LXBhc3M=
---
apiVersion: v1
kind: Service
metadata:  { name: mysql-headless }
spec:
  clusterIP: None
  selector:  { app: mysql }
  ports:  [{ port: 3306 }]
---
apiVersion: apps/v1
kind: StatefulSet
metadata:  { name: mysql }
spec:
  serviceName: mysql-headless
  replicas: 3
  selector:
    matchLabels:  { app: mysql }
  template:
    metadata:
      labels:  { app: mysql }
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: mysql
        image: mysql:8.0
        ports:  [{ containerPort: 3306 }]
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: root-password
        resources:
          requests: { cpu: "500m",  memory: "512Mi" }
          limits:   { cpu: "1000m", memory: "1Gi"   }
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
        readinessProbe:
          exec:
            command: ["mysqladmin","ping","-h","127.0.0.1"]
          initialDelaySeconds: 30
  volumeClaimTemplates:
  - metadata:  { name: mysql-data }
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: standard
      resources:
        requests:  { storage: 10Gi }

How Scaling Works Differently

Stateless Scaling

# Scale up (instant, parallel)
$ kubectl scale deployment nginx \
    --replicas=10

# Scale down (any order)
$ kubectl scale deployment nginx \
    --replicas=2

Stateful Scaling

# Scale up (ONE by ONE)
$ kubectl scale statefulset mysql \
    --replicas=5

# Scale down (REVERSE order)
$ kubectl scale statefulset mysql \
    --replicas=2
# PVCs NOT deleted!
Key Difference

Stateless: Pods gone, no data loss. Stateful: Pods gone but PVCs kept. Scale back up = data returns. (You still pay for disks!)

When to Use Which?

Use Deployment (Stateless)

  • App doesn't save data locally
  • All pods are identical
  • Losing a pod is no problem
  • Easy, fast scaling needed
  • Data lives in external DB

Use StatefulSet (Stateful)

  • Each pod needs own storage
  • Stable DNS names needed
  • Database or message broker
  • Ordered startup/shutdown
  • Master-replica topology
Pro Tip

Most apps are deployed stateless, connecting to managed database services (AWS RDS, Cloud SQL). This avoids StatefulSet complexity entirely!

Common Beginner Mistakes

Mistake 1: Deployment for a database

Pod restart = ALL DATA GONE. Use StatefulSet.

Mistake 2: Forgetting Headless Service

StatefulSet requires it. No headless = no stable DNS.

Mistake 3: No resource limits

One pod eats all resources, crashes everything.

Mistake 4: Passwords in plain YAML

Use Kubernetes Secrets. Never commit passwords.

Mistake 5: Expecting auto-replication

StatefulSet manages pods/storage/names only. Replication = your job.

Mistake 6: Deleting PVCs accidentally

PVCs survive StatefulSet deletion (safety). Manual delete = data gone forever.

Cheat Sheet & Quick Reference

kubectl Commands

# ===== STATELESS =====
kubectl create deployment nginx --image=nginx
kubectl get deployments
kubectl scale deployment nginx --replicas=5
kubectl rollout status deployment/nginx
kubectl rollout undo deployment/nginx

# ===== STATEFUL =====
kubectl get statefulsets
kubectl get pvc
kubectl scale statefulset mysql --replicas=5
kubectl delete statefulset mysql      # PVCs remain!

# ===== GENERAL =====
kubectl get pods -w                   # Watch live
kubectl describe pod mysql-0
kubectl logs mysql-0
kubectl exec -it mysql-0 -- bash
kubectl get all

🛠 Decision Flowchart

Does your app SAVE DATA locally?
NO
🚀 DEPLOYMENT
Stateless
YES
Needs unique names
or stable DNS?
NO
🚀 Deployment
YES
🔒 StatefulSet
You Made It!

Stateless = simple, replaceable. Stateful = complex, data-safe. Start with stateless. When comfortable, explore stateful. Practice with Minikube or Kind!