Kubernetes Workload Resources: When to Use Deployments, StatefulSets, DaemonSets, and Jobs
Kubernetes has six workload resource types. Each represents a different scheduling and lifecycle contract. Choosing the wrong one — a Deployment for a database, a StatefulSet for a stateless API — creates subtle operational problems that don't surface until scale or failure.
The six workload types and what they promise
Every Kubernetes workload wraps a pod template. The workload type controls how many instances run, how they're replaced, and what guarantees are made about identity and storage.
| Resource | Pods | Identity | Storage | Use case | |---|---|---|---|---| | Deployment | N interchangeable | Random names | Shared or none | Stateless services | | StatefulSet | N ordered | Stable names | Per-pod PVCs | Databases, brokers | | DaemonSet | 1 per node | Per-node | Host path or none | Log agents, monitoring | | Job | 1+ to completion | Temporary | Temporary | Batch processing | | CronJob | Job on schedule | Temporary | Temporary | Scheduled tasks | | ReplicaSet | N (rarely direct) | Random | Shared or none | Managed by Deployment |
Deployment: stateless services with rolling updates
ConceptKubernetesA Deployment manages a ReplicaSet that keeps N identical pod replicas running. Pods are fungible — no stable identity, no persistent storage tied to individual pods. Rolling updates replace pods one by one. Scale up or down by changing replicas count.
Prerequisites
- Kubernetes pods
- ReplicaSets
- rolling updates
Key Points
- Pod names are randomly generated: my-api-7d9f4b6c5-x2q8p. No stable identity between restarts.
- Rolling update: maxUnavailable controls minimum available during update, maxSurge controls temporary overcapacity.
- All pods share the same configuration. Horizontal scaling adds more identical pods.
- Rollback to previous revision: kubectl rollout undo deployment/my-api.
Deployment: the right choice for stateless services
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # at least 2 pods available during update
maxSurge: 1 # at most 4 pods total during update
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: myapp:1.2.3
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1Gi"
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
Deployments are the right choice when: all pods are equivalent, storage is external (RDS, Redis, S3), and the service can be load-balanced across any instance.
StatefulSet: stable identity for stateful applications
StatefulSets make three guarantees Deployments don't:
- Stable, predictable pod names:
postgres-0,postgres-1,postgres-2 - Ordered startup (0 before 1 before 2) and ordered shutdown (reverse)
- Per-pod persistent volume claims that survive pod replacement
The stable DNS names are the critical feature for distributed systems. A PostgreSQL replica can be configured to replicate from postgres-0.postgres-headless.default.svc.cluster.local — this DNS name resolves to the same pod regardless of restarts and rescheduling.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres-headless # must exist as a headless Service
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates: # each pod gets its own PVC
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: gp3
resources:
requests:
storage: 100Gi
DaemonSet: one pod per node
DaemonSets ensure exactly one pod runs on every node (or every node matching a node selector). When new nodes join the cluster, the DaemonSet controller automatically schedules a pod on them.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: log-collector
spec:
selector:
matchLabels:
app: log-collector
template:
metadata:
labels:
app: log-collector
spec:
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule # run on control plane nodes too if needed
containers:
- name: fluentd
image: fluentd:v1.16
volumeMounts:
- name: varlog
mountPath: /var/log
- name: dockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
volumes:
- name: varlog
hostPath:
path: /var/log
- name: dockercontainers
hostPath:
path: /var/lib/docker/containers
DaemonSets use hostPath volumes to access node-local paths. This is why they need DaemonSet — a Deployment doesn't guarantee one pod per node, and wouldn't have access to per-node paths in a predictable way.
📝Jobs and CronJobs: run-to-completion semantics
Jobs run a pod until it completes successfully. Unlike Deployments, a finished Job doesn't restart. This makes them appropriate for database migrations, batch data processing, and one-time initialization tasks.
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
spec:
completions: 1 # run 1 successful completion
parallelism: 1 # run 1 pod at a time
backoffLimit: 3 # retry up to 3 times on failure
template:
spec:
restartPolicy: OnFailure # required for Jobs (Never or OnFailure only)
containers:
- name: migrate
image: myapp:1.2.3
command: ["python", "manage.py", "migrate"]
envFrom:
- secretRef:
name: db-credentials
CronJobs create Jobs on a schedule:
apiVersion: batch/v1
kind: CronJob
metadata:
name: cleanup
spec:
schedule: "0 2 * * *" # 2 AM daily
concurrencyPolicy: Forbid # don't start new Job if previous is still running
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: cleanup
image: myapp:1.2.3
command: ["python", "cleanup.py"]
Common CronJob problems:
concurrencyPolicy: Allow(default) lets multiple Jobs run simultaneously if the previous one hasn't finished. For database cleanup jobs, this causes conflicts.successfulJobsHistoryLimit: 3— keep this low. Without it, finished pod objects accumulate.- Missing resource requests means CronJob pods may not schedule when the cluster is under resource pressure.
HorizontalPodAutoscaler: scaling based on metrics
HPAs automatically adjust Deployment (or StatefulSet) replica counts based on observed metrics:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # scale up when average CPU > 70%
behavior:
scaleUp:
stabilizationWindowSeconds: 30 # react quickly to traffic spikes
scaleDown:
stabilizationWindowSeconds: 300 # wait 5 min before scaling down
HPAs require resource requests to be set on containers — without them, CPU utilization percentage is undefined and HPA won't work.
A team runs a Redis cluster in EKS using a Deployment with 3 replicas. During a node failure, two Redis pods restart on different nodes. After the incident, Redis reports cluster state errors and data inconsistency. Why is Deployment the wrong choice here, and what should be used instead?
mediumThe Redis cluster uses cluster mode. Each Redis instance is configured to communicate with the others using their pod names. The Deployment uses a shared PVC (ReadWriteMany EFS volume) for persistence.
ADeployments don't support Redis — use a Helm chart instead
Incorrect.Helm charts still use underlying Kubernetes resources. The issue is the resource type chosen, not the deployment method.BDeployment pods get new random names on restart. Redis cluster peers are configured by pod name — the new names break cluster membership. StatefulSet provides stable pod names that survive restart
Correct!Redis cluster mode members discover each other by their addresses. A Deployment's pod redis-7f9d-abc becomes redis-4b2e-xyz after restart — the old address is gone. Other cluster members still reference the old name, causing cluster split-brain. StatefulSet gives redis-0, redis-1, redis-2 — stable DNS names that survive restarts. Each pod also needs its own dedicated PVC via volumeClaimTemplates, not a shared EFS volume (Redis doesn't support multiple writers to the same data directory).CDeployments can't use persistent volumes — that's why data was lost
Incorrect.Deployments can use persistent volumes. The issue is pod identity stability and per-pod storage, not whether PVs can be used.DRedis should run as a DaemonSet for cluster mode
Incorrect.DaemonSet places one pod per node — the cluster would scale with node count rather than independent replica count, and couldn't be scaled independently.
Hint:Think about what changes when a Deployment pod restarts vs a StatefulSet pod restarts.