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:

  1. Does the workload run continuously or to completion? Long-running services need Deployment, StatefulSet, or DaemonSet. Tasks that finish need Job or CronJob.
  2. 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.
  3. Must one instance run on every node? If the workload is a per-node agent, use DaemonSet.
  4. 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  --> Deployment

Deployment#

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: 256Mi

StatefulSet#

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: 10Gi

Key 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/log

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