Choosing Kubernetes Workload Types#
Kubernetes provides several workload controllers, each designed for a specific class of application behavior. Choosing the wrong one leads to data loss, unnecessary complexity, or workloads that fight the platform instead of leveraging it. This guide walks through the decision criteria and tradeoffs for each type.
The Workload Types at a Glance#
| Workload Type | Lifecycle | Pod Identity | Scaling Model | Storage Model | Typical Use |
|---|---|---|---|---|---|
| Deployment | Long-running | Interchangeable | Horizontal replicas | Shared or none | Web servers, APIs, stateless microservices |
| StatefulSet | Long-running | Stable, ordered | Ordered horizontal | Per-pod persistent | Databases, message queues, distributed consensus |
| DaemonSet | Long-running | One per node | Tied to node count | Node-local | Log collectors, monitoring agents, network plugins |
| Job | Run to completion | Disposable | Parallel completions | Ephemeral | Batch processing, migrations, one-time tasks |
| CronJob | Scheduled | Disposable | Per-schedule run | Ephemeral | Periodic backups, cleanup, scheduled reports |
| ReplicaSet | Long-running | Interchangeable | Horizontal replicas | Shared or none | Almost never used directly |
Decision Criteria#
The choice comes down to four questions:
- Does the workload run continuously or to completion? Long-running services need Deployment, StatefulSet, or DaemonSet. Tasks that finish need Job or CronJob.
- Does each instance need a unique, stable identity? If pods must be distinguishable from each other (stable hostname, dedicated storage), use StatefulSet. If pods are interchangeable, use Deployment.
- Must one instance run on every node? If the workload is a per-node agent, use DaemonSet.
- Is the workload triggered on a schedule? If it runs periodically, use CronJob.
Decision Flowchart#
Start
|
+--> Does the workload run to completion?
| |
| +--> YES: Is it triggered on a schedule?
| | |
| | +--> YES --> CronJob
| | +--> NO --> Job
| |
| +--> NO (long-running):
| |
| +--> Must it run on every (or selected) node?
| | |
| | +--> YES --> DaemonSet
| | +--> NO:
| | |
| | +--> Does each pod need stable identity
| | | or dedicated persistent storage?
| | | |
| | | +--> YES --> StatefulSet
| | | +--> NO --> DeploymentDeployment#
Deployments manage stateless, interchangeable replicas. Pods get random names, can be killed and replaced freely, and share no ordering guarantees. Kubernetes manages rolling updates, rollbacks, and scaling through an underlying ReplicaSet.
Choose Deployment when:
- The application is stateless or externalizes its state (to a database, cache, or object store).
- Pods are interchangeable – any replica can handle any request.
- You need horizontal scaling with rolling updates.
- Examples: REST APIs, web frontends, GraphQL servers, gRPC microservices, queue consumers that checkpoint externally.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
containers:
- name: api-server
image: api-server:1.4.0
resources:
requests:
cpu: 250m
memory: 256MiStatefulSet#
StatefulSets give each pod a stable hostname (pod-0, pod-1, pod-2), ordered creation and deletion, and dedicated persistent volumes that survive pod restarts. Pods are not interchangeable.
Choose StatefulSet when:
- Each replica needs a stable network identity (e.g.,
postgres-0.postgres-headless.default.svc.cluster.local). - Each replica needs its own persistent volume that follows it across rescheduling.
- Startup and shutdown order matters (leader election, primary/replica topology).
- Examples: PostgreSQL, MySQL, MongoDB, Redis Sentinel, Kafka brokers, ZooKeeper, Elasticsearch, etcd.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres-headless
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: standard
resources:
requests:
storage: 10GiKey tradeoff: StatefulSets are more complex to operate. Scaling down does not delete PVCs (by default), updates are ordered and slower, and pod rescheduling is constrained by volume availability. Do not use a StatefulSet if a Deployment would suffice.
DaemonSet#
DaemonSets ensure exactly one pod runs on every node (or a subset selected by node selectors and tolerations). When nodes are added, the DaemonSet automatically schedules a pod. When nodes are removed, the pod is garbage collected.
Choose DaemonSet when:
- The workload is a per-node agent that must run on every node.
- The workload needs access to node-level resources (host filesystem, host network, device plugins).
- Examples: Fluentd/Fluent Bit log collectors, Datadog/Prometheus node exporters, Calico/Cilium CNI agents, CSI node drivers, kube-proxy.
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
containers:
- name: fluent-bit
image: fluent/fluent-bit:3.0
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
volumes:
- name: varlog
hostPath:
path: /var/logKey tradeoff: You do not control replica count directly – it is determined by node count. Scaling the DaemonSet means scaling the cluster. If you need N replicas independent of node count, use a Deployment.
Job#
Jobs run pods to completion. Once all pods succeed, the Job is done. Jobs support parallelism (running multiple pods at once) and indexed completion (assigning each pod a unique index for partitioned work).
Choose Job when:
- The workload has a defined end: it processes input, produces output, and exits.
- You need Kubernetes to handle retries on failure.
- Examples: database migrations, ETL batch processing, ML training runs, report generation, data backups, one-time administrative tasks.
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
spec:
backoffLimit: 3
activeDeadlineSeconds: 600
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: app:1.4.0
command: ["./migrate", "--target", "latest"]CronJob#
CronJobs are Jobs triggered on a cron schedule. Each trigger creates a new Job object.
Choose CronJob when:
- The workload needs to run periodically on a schedule.
- Examples: nightly database backups, hourly report generation, daily cleanup tasks, periodic certificate rotation.
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-backup
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: backup-tool:1.0
command: ["./backup.sh"]ReplicaSet#
ReplicaSets maintain a stable set of pod replicas. Almost never use a ReplicaSet directly. Deployments manage ReplicaSets for you and add rolling update and rollback capabilities. The only reason to create a bare ReplicaSet is if you need custom update orchestration, which is rare.
Common Mistakes#
Using Deployment for databases. A Deployment with a PVC technically works for a single-replica database, but you lose ordered shutdown guarantees and stable identity. If you ever need to scale to multiple replicas, you must switch to StatefulSet. For anything beyond throwaway dev databases, start with StatefulSet.
Using StatefulSet when Deployment would work. If your application externalizes all state and pods are interchangeable, a StatefulSet adds complexity with no benefit. The ordered rollout is slower, PVC lifecycle is harder to manage, and debugging is more involved.
Running batch work as a Deployment with a sleep loop. If your application processes a queue and you keep it running in a Deployment with a loop that sleeps when idle, consider KEDA or a Job-based pattern instead. Sleep loops waste resources and complicate lifecycle management.
Ignoring CronJob concurrencyPolicy. The default is Allow, which means if a previous run is still going when the next schedule triggers, both run simultaneously. Set Forbid if your job cannot safely overlap, or Replace if the new run should cancel the old one.
Hybrid Patterns#
Deployment + PVC: A Deployment with a single replica and a PVC works for applications that need persistent storage but not stable identity. This is common for applications with a local cache directory or SQLite database. The limitation is that you cannot scale beyond one replica sharing the same PVC (unless using ReadWriteMany, which most storage classes do not support).
Job with indexed completion: For parallel batch processing where each pod handles a partition of the work, use an indexed Job. Each pod receives its index as an environment variable and processes the corresponding data slice.
apiVersion: batch/v1
kind: Job
metadata:
name: parallel-process
spec:
completionMode: Indexed
completions: 10
parallelism: 5
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: batch-worker:1.0
# JOB_COMPLETION_INDEX env var is set automatically (0-9)StatefulSet + sidecar for metrics: StatefulSets often need per-instance monitoring. Because each pod has a stable identity, you can build dashboards that track individual replicas (e.g., “postgres-0 replication lag”) rather than aggregating across anonymous pods.
Summary#
| If your workload… | Use |
|---|---|
| Is stateless and long-running | Deployment |
| Needs stable identity or per-pod storage | StatefulSet |
| Must run on every node | DaemonSet |
| Runs to completion | Job |
| Runs on a schedule | CronJob |
| Needs custom update orchestration (rare) | ReplicaSet |
Start with the simplest option that meets your requirements. Deployment covers the majority of workloads. Escalate to StatefulSet or DaemonSet only when the application genuinely requires their guarantees.