Kubernetes Workload Resources: Deployments, StatefulSets, and DaemonSets
Understand Kubernetes workload resources—when to use Deployments for stateless apps, StatefulSets for clustered workloads, and DaemonSets for node-level agents.
Kubernetes Workload Resources: Deployments, StatefulSets, and DaemonSets
If you are running applications on Kubernetes, you need a way to manage the pods running your code. Kubernetes does not just schedule pods onto nodes and leave them there. It provides workload resources that handle replication, scaling, rolling updates, and fault tolerance. This post walks through the three most important workload types: Deployment, StatefulSet, and DaemonSet.
If you are new to Kubernetes, start with the Kubernetes fundamentals post before diving into workloads. For advanced orchestration patterns, check the Advanced Kubernetes post.
Introduction
Kubernetes does not just schedule pods onto nodes and leave them there. Workload resources handle replication, scaling, rolling updates, and fault tolerance. Pick the wrong one and your database corrupts data, your log collector runs everywhere, or updates break in ways that are hard to recover from.
The Deployment vs StatefulSet decision shows up constantly in audits. Running Postgres as a Deployment is a fast track to data corruption. Multiple pods writing to the same volume is not safe. Similarly, running a log collector as a Deployment instead of a DaemonSet means you have replicas you did not ask for.
This post walks through the three workload types that matter most: Deployment for stateless services, StatefulSet for databases and message queues, and DaemonSet for node-level agents. You will learn which to pick, how their scaling and update strategies differ, and what breaks when you pick wrong.
When to Use / When Not to Use
Use this decision tree to pick the right workload resource:
Does your application need stable network identity
or persistent storage across restarts?
├── NO → Is it a node-level agent (logging, monitoring, networking)?
│ ├── YES → DaemonSet
│ └── NO → Deployment
└── YES → Is it a database, message queue, or leader-elected service?
├── YES → StatefulSet
└── NO (you just need scaling) → Consider if Deployment suffices
Use a Deployment when: web applications, stateless APIs, queue workers. Any workload where pods are interchangeable.
Skip Deployment when: you need stable hostnames, ordered startup, or persistent storage across restarts.
Use a StatefulSet when: you are running databases (PostgreSQL, MySQL, MongoDB), message queues (Kafka, RabbitMQ), or leader-elected services (ZooKeeper, etcd). If the pod name determines which data is yours, you need StatefulSet.
Skip StatefulSet when: your app is stateless. StatefulSets add real complexity around scaling and storage. If you do not need stable identity, Deployment is simpler.
Use a DaemonSet when: you need something on every node. Log collectors, node exporters, CNI plugins, storage daemons. These are infrastructure concerns, not application concerns.
Skip DaemonSet when: your workload scales with user load, not node count. Use Deployment instead.
ReplicaSet and Deployment Patterns
A ReplicaSet makes sure a specific number of identical pod replicas are running at any given time. You rarely create ReplicaSets directly. The Deployment wraps a ReplicaSet and adds declarative update capabilities on top.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-frontend
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: web-frontend
template:
metadata:
labels:
app: web-frontend
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
resources:
requests:
memory: "128Mi"
cpu: "250m"
limits:
memory: "256Mi"
cpu: "500m"
The Deployment controller watches the ReplicaSet and creates a new ReplicaSet whenever you update the pod template. It also keeps a revision history so you can roll back if something goes wrong.
kubectl rollout history deployment/web-frontend
kubectl rollout undo deployment/web-frontend
kubectl rollout undo deployment/web-frontend --to-revision=2
Deployments work well for stateless applications where each replica is interchangeable. You do not need persistent storage or a fixed network identity for each replica.
Scaling a Deployment
kubectl scale deployment web-frontend --replicas=5
Or update the spec directly:
kubectl patch deployment web-frontend -p '{"spec":{"replicas":5}}'
The maxSurge and maxUnavailable fields control update behavior:
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
With these settings, Kubernetes adds one new pod before removing an old one. Zero downtime updates become straightforward.
Rolling Updates and Rollback Strategies
Rolling updates proceed pod by pod. Kubernetes replaces old pods with new ones while keeping enough replicas running to handle traffic.
The minReadySeconds field tells Kubernetes how long to wait before marking a pod as ready:
spec:
minReadySeconds: 30
progressDeadlineSeconds: 600
If a pod fails to become ready within progressDeadlineSeconds, Kubernetes stops the rollout and reports a condition.
For databases or stateful services, rolling updates do not work the same way. You need to think carefully about schema migrations and data consistency. This is where StatefulSets become relevant.
StatefulSet Identity and Stable Storage
StatefulSets give each pod a persistent identity. Pods have stable network names and persistent storage that survives restarts. This matters for clustered databases, message queues, and leader-elected services.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres-cluster
namespace: database
spec:
serviceName: "postgres-cluster"
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:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "fast-ssd"
resources:
requests:
storage: 50Gi
Each pod gets a predictable name: postgres-cluster-0, postgres-cluster-1, postgres-cluster-2. The volume claims persist even when pods reschedule to different nodes.
StatefulSets support ordered deployment and scaling. Pod 1 will not start until Pod 0 is running and ready. This ordering matters for primary-secondary database clusters where you need to establish quorum before adding replicas.
Managing StatefulSet Scaling
kubectl scale statefulset postgres-cluster --replicas=5
The new pods provision their own persistent volumes. Scaling down removes pods in reverse order, but persistent volumes do not get deleted automatically. You need to handle data migration before scaling down.
DaemonSet for Cluster-Wide Agents
A DaemonSet runs one pod on every node (or on nodes matching a selector). This makes sense for log collectors, monitoring agents, and node-level services that need to be present on every machine.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd-log-collector
namespace: monitoring
spec:
selector:
matchLabels:
app: fluentd
template:
metadata:
labels:
app: fluentd
spec:
containers:
- name: fluentd
image: fluent/fluentd:v1.16
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
tolerations:
- key: "node-role.kubernetes.io/control-plane"
operator: "Exists"
effect: "NoSchedule"
Notice the toleration for control-plane nodes. By default, DaemonSets do not schedule onto control-plane nodes. Add tolerations if you need to run on those nodes too.
You can also restrict DaemonSets to specific nodes using node selectors or node affinity:
spec:
template:
spec:
nodeSelector:
disktype: ssd
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: "topology.kubernetes.io/zone"
operator: In
values:
- us-east-1a
DaemonSets automatically scale when you add nodes to the cluster. The controller creates pods on the new nodes without any additional action from you.
Choosing the Right Workload
The choice comes down to your application characteristics:
| Workload | When to Use | Identity | Storage |
|---|---|---|---|
| Deployment | Stateless services, web apps, APIs | None (pods are interchangeable) | Ephemeral |
| StatefulSet | Databases, message queues, leader-elected services | Stable network names, ordered deployment | Persistent via volumeClaimTemplates |
| DaemonSet | Node-level agents, log collectors, monitoring | None (one per node) | Depends on configuration |
Do not use a StatefulSet when a Deployment would suffice. StatefulSets add complexity around scaling, updates, and storage management. If your application does not need stable identity or persistent storage, stick with a Deployment.
For general guidance on Kubernetes architecture, see the Kubernetes fundamentals post. For more advanced patterns like custom controllers and operators, check the Advanced Kubernetes post.
Production Failure Scenarios
StatefulSet Fails to Scale Due to Volume Binding Issues
When a StatefulSet tries to scale up, the PVC provisioner may fail if the StorageClass cannot match the PVC requirements or the cloud quota is exhausted.
Symptoms: StatefulSet does not scale, Pod remains Pending, VolumeBinding unknown in describe output.
Diagnosis:
kubectl describe statefulset postgres-cluster -n database
kubectl get events --sort-by='.lastTimestamp' -n database
kubectl get pvc -n database # Check bound status
Mitigation: Pre-provision PVs for known storage needs. Set StorageClass volumeBindingMode: WaitForFirstConsumer to avoid cross-zone binding issues. Monitor cloud storage quotas.
DaemonSet Not Scheduling on Control-Plane Nodes
DaemonSets ignore control-plane nodes by default. The taint keeps your infrastructure pods off the control plane, which is usually what you want. But CNI plugins and some monitoring agents need to run there too.
Symptoms: kubectl get daemonset shows fewer nodes than your cluster has.
Mitigation: Add this toleration to your DaemonSet spec:
tolerations:
- key: "node-role.kubernetes.io/control-plane"
operator: "Exists"
effect: "NoSchedule"
Anti-Patterns
Using Deployment for Databases
This one comes up constantly in audits. Running PostgreSQL as a Deployment will eventually corrupt your data. Multiple pods trying to write to the same volume is not a safe setup. Use StatefulSet instead.
Not Setting minReadySeconds
If you skip minReadySeconds, Kubernetes considers a pod Ready the instant its containers start, not when the application inside is actually ready to handle traffic. Your load balancer will send requests to pods that are still spinning up.
Set minReadySeconds to at least your application’s worst-case startup time.
Setting Identical Requests and Limits for All Pods
Giving every pod the same resources is lazy. A web server and a batch job have completely different resource profiles. Profile your workloads first, then set appropriate values.
Trade-off Analysis
Workload Resource Trade-offs
| Factor | Deployment | StatefulSet | DaemonSet |
|---|---|---|---|
| Scaling | Horizontal, interchangeable replicas | Horizontal with ordered scaling | One per node (fixed ratio) |
| Storage | Ephemeral (emptyDir) | Persistent via volumeClaimTemplates | Node-local or attached |
| Network identity | None (pods interchangeable) | Stable network names (ordinal-based) | None (one per node) |
| Startup order | No guarantees | Ordered (pod N waits for N-1) | All nodes simultaneously |
| Upgrade strategy | RollingUpdate with maxSurge | RollingUpdate with ordered replacement | On Delete (no rolling) |
| Use case | Stateless web apps, APIs, workers | Databases, message queues, leader-elected | Node-level agents, log collectors |
| Complexity | Low | High | Low |
| Failure recovery | Restart pod, reschedule anywhere | Restores to same node with same identity | Automatic on new node |
Deployment vs StatefulSet: If pods are interchangeable and you just need scaling, use Deployment. If you need stable identity or persistent storage across restarts, use StatefulSet. The complexity of StatefulSet (ordered deployment, graceful scaling, storage management) is only justified when your application genuinely requires it.
DaemonSet vs Deployment: If your workload scales with user load, use Deployment. If your workload runs on every node regardless of load (logging, monitoring, networking), use DaemonSet. DaemonSets are infrastructure concerns, not application concerns.
Scaling Considerations
Scaling Deployments is straightforward: change replica count and Kubernetes creates or destroys pods. StatefulSet scaling is more nuanced — adding replicas provisions new volumes but removing replicas does not delete volumes (data persists). This is intentional: you handle data migration before scaling down.
For StatefulSets, the ordinal nature of pod names means you can predict which pod is which. This is essential for databases that need to establish primary-replica relationships before accepting writes.
Interview Questions
Never. Deployments are inappropriate for databases because pods are interchangeable and each pod would try to write to the same persistent volume, causing data corruption. StatefulSets are the correct choice because they provide stable network identities (predictable hostnames like postgres-0, postgres-1) and persistent storage that survives pod restarts. The ordered deployment and scaling behavior ensures primary-replica relationships are established correctly.
StatefulSet pods deploy in ordinal order: pod N does not start until pod N-1 is running and ready. This ordering matters for clustered databases and leader-elected services where you need to establish quorum before adding replicas. For example, in a 3-node PostgreSQL cluster, the second pod cannot start until the first is fully initialized and ready to accept replication connections. Without ordered deployment, the second pod might try to connect to a primary that does not exist yet.
Add a toleration to the DaemonSet pod template spec. Control-plane nodes have a taint `node-role.kubernetes.io/control-plane:NoSchedule`. To tolerate this taint and allow your DaemonSet pods to schedule on control-plane nodes, add this toleration to your DaemonSet spec: `tolerations: - key: "node-role.kubernetes.io/control-plane" operator: "Exists" effect: "NoSchedule"`. This tells Kubernetes to ignore the control-plane taint for this DaemonSet.
When a StatefulSet pod is deleted (voluntary or due to node failure), the PVC persists and a new pod is created with the same name. When the new pod starts, it attaches to the same PVC and continues with the existing data. This is the behavior that provides stable storage identity. However, if the PVC's binding to a PV becomes unhealthy (for example, the volume is stuck in a trailing state), the pod may get stuck in Pending. In this case, you need to investigate the PVC and PV state.
`maxSurge` is the number of extra pods above the desired count during an update. `maxUnavailable` is the number of pods that can be unavailable below the desired count during an update. For zero-downtime updates, set `maxUnavailable: 0` and `maxSurge: 1` — Kubernetes creates the new pod before terminating the old one, maintaining full capacity throughout. For faster updates at the cost of temporary reduced capacity, increase `maxUnavailable` and decrease `maxSurge`.
When a new node joins the cluster, the DaemonSet controller automatically creates pods on the new node to match the desired state. When a node is removed (drained or fails), the DaemonSet pods on that node are terminated normally (respecting terminationGracePeriodSeconds). The key insight: DaemonSets do not automatically clean up after themselves when nodes leave — if a node comes back, the DaemonSet recreates the pods. But if a node is permanently removed, its DaemonSet pods are simply gone with no rescheduling.
`minReadySeconds` tells Kubernetes how long to wait before marking a pod as ready after its containers start. Without this setting, Kubernetes considers a pod Ready the instant containers start, not when the application inside is ready to handle traffic. This means your load balancer sends requests to pods that are still initializing. Set `minReadySeconds` to match your application's worst-case startup time. For example, if your application takes up to 30 seconds to warm up, set `minReadySeconds: 30`.
Start by describing the StatefulSet to see events: `kubectl describe statefulset postgres-cluster -n database`. Check the events sorted by time: `kubectl get events --sort-by='.lastTimestamp' -n database`. Check PVC status to see if volumes are bound: `kubectl get pvc -n database`. Common causes: StorageClass cannot satisfy the PVC requirements, cloud quota is exhausted for persistent disk, zone restrictions prevent volume placement, or the volume provisioner is slow or failing. Pre-provisioning volumes or using `volumeBindingMode: WaitForFirstConsumer` can prevent cross-zone binding issues.
Avoid DaemonSets when your workload scales with user load rather than node count. DaemonSets create one pod per node regardless of load — if you have 50 nodes and your API only needs 10 pods under current load, the DaemonSet wastes resources. Also avoid DaemonSets for application workloads that need horizontal scaling with automatic load balancing — Deployment with HPA is the right tool for that. DaemonSets are for infrastructure concerns (logging, monitoring, networking) that belong on every machine, not for application logic that scales elastically.
A ReplicaSet ensures a specific number of identical pod replicas are running. A Deployment manages ReplicaSets and adds declarative update capabilities on top. When you update a Deployment's pod template, it creates a new ReplicaSet, gradually scales it up while scaling the old one down, and keeps the old ReplicaSet for rollback capability. You rarely create ReplicaSets directly because Deployments provide this higher-level orchestration. Creating a ReplicaSet directly bypasses the update and rollback management that Deployments provide.
Use `kubectl rollout history deployment/name` to see revision history, then `kubectl rollout undo deployment/name` to roll back to the previous version, or `kubectl rollout undo deployment/name --to-revision=N` to roll back to a specific revision. The Deployment controller keeps revision history so you can navigate back through changes. Note: each update to the pod template (image, environment variables, etc.) creates a new revision. If a deployment goes wrong, undo quickly before more revisions accumulate.
The primary anti-pattern is using Deployment for databases or any workload requiring persistent storage. Multiple Deployment pods writing to the same volume will corrupt data. Other anti-patterns: setting identical resource requests and limits for all pods when workloads have different profiles, not setting `minReadySeconds` so Kubernetes considers pods ready before applications are, using `latest` image tags causing unpredictable rollouts, and not setting PodDisruptionBudgets so node drains can take down too many replicas at once.
A DaemonSet runs one pod per node, shared across all workloads on that node. A sidecar runs as part of each individual pod's workload, meaning you have N copies where N is the number of application pods, not the number of nodes. For node-level concerns like log collection and monitoring, DaemonSet is more efficient: one pod per node regardless of how many application pods run there. For workload-specific proxying or logging tied to a specific application instance, sidecars are appropriate.
Add a `nodeSelector` to the DaemonSet pod template spec to match nodes by label. For example: `nodeSelector: disktype: ssd` schedules the DaemonSet only on nodes with the `disktype=ssd` label. For more complex rules, use `nodeAffinity` which supports required vs preferred rules and can match multiple expressions. This lets you ensure node-level agents only run on appropriate infrastructure — for example, storage daemons on nodes with fast disks, or monitoring agents on nodes in specific zones.
When you scale down a StatefulSet, pods are terminated in reverse ordinal order (highest first). The persistent volumes and PVCs are not automatically deleted — they persist. This is intentional because scaling down might be temporary (scale up later brings pods back with same storage). You must manually handle data migration before scaling down if you want to reclaim the storage. The volumes remain bound to their PVs until explicitly deleted. This protects against accidental data loss but requires explicit cleanup when you really want to release storage.
ReadinessGates are additional conditions beyond the container-level readinessProbe that determine if a pod is ready to receive traffic. They allow custom readiness checks that the standard probe types (HTTP, TCP, exec) cannot express. Use readinessGates when your application needs external dependencies (like a service it depends on being available) before it can accept traffic. The pod becomes ready only when both the readinessProbe AND all readinessGates are satisfied. This is useful for applications that cannot gracefully handle traffic until all their dependencies are ready.
`progressDeadlineSeconds` sets the time Kubernetes waits for a rollout to make progress before reporting a failure condition on the Deployment. If the Deployment does not complete within this time (new pods are not ready, old pods not terminated), Kubernetes marks the Deployment as Failed and stops trying. This prevents a stuck deployment from blocking further updates indefinitely. Set it long enough for your application to handle worst-case startup time plus some buffer. Default is 600 seconds (10 minutes).
The Deployment controller creates a new ReplicaSet whenever the pod template spec (`.spec.template`) changes. This includes changes to image tags, environment variables, volume mounts, or any other container configuration. The selector (`.spec.selector`) does not change — it stays the same across all ReplicaSets so the Service continues routing to ready pods. Each change creates a new ReplicaSet and triggers a rolling update. The old ReplicaSet is kept (but scaled to 0) for potential rollback.
Deleting a Deployment terminates pods and deletes ReplicaSets, but persistent volumes and claims persist (they are namespaced separately). Deleting a StatefulSet also terminates pods, but the behavior on PVCs depends on the reclaim policy: if the PVC's storage class has `retention: Delete`, volumes are deleted; if `retention: Retain`, volumes persist even after StatefulSet deletion. Always check your storage class reclaim policy and explicitly handle data migration before deleting if the volumes contain important data.
Set requests and limits in the container spec: `resources: requests: memory: "128Mi" cpu: "250m" limits: memory: "256Mi" cpu: "500m"`. Requests guarantee the minimum; limits cap the maximum. Without requests, the scheduler may place pods on nodes that cannot provide enough resources, causing throttling or OOM kills. Without limits, a pod can consume all resources on a node, starving other pods. Profile your workloads under realistic load and set values with headroom. Use LimitRanges to enforce default limits per namespace so new containers get sensible defaults automatically.
Further Reading
- Kubernetes Documentation: Workload Resources
- Kubernetes Documentation: Deployments
- Kubernetes Documentation: StatefulSets
- Kubernetes Documentation: DaemonSets
- Kubernetes Documentation: Pod Disruption Budgets
Advanced Workload Patterns
Job-based batch processing: For batch workloads that run to completion, use Jobs or CronJobs instead of Deployments. A Job creates pods that run until completion, while a CronJob schedules recurring batch jobs. This is appropriate for data processing, report generation, and one-time migration tasks.
Init containers for workload setup: Use init containers to prepare the workload environment before the main container starts. Init containers run to completion before the main container starts, which is useful for database schema migrations, configuration loading, or dependency waiting that must complete before the application runs.
Pod topology spread constraints: Distribute workload replicas across failure domains (zones, nodes) for high availability:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web-frontend
This ensures pods spread evenly across zones, preventing zone failures from taking down your entire service.
Conclusion
Use this checklist when choosing and configuring Kubernetes workload resources:
- Chose Deployment for stateless services, StatefulSet for databases/stateful apps, DaemonSet for node agents
- Set
minReadySecondsto match application startup time - Configured
maxSurgeandmaxUnavailablefor zero-downtime rolling updates - Used StatefulSet with
volumeClaimTemplatesfor persistent storage needs - Added tolerations to DaemonSet if scheduling on control-plane nodes is needed
- Set resource requests and limits for all containers
- Used
readinessGatefor applications that need additional health checks beyond container-level probes - Tested scaling behavior in staging before production
- Monitored StatefulSet ordered scaling behavior when adding or removing replicas
Category
Related Posts
Container Security: Image Scanning and Vulnerability Management
Implement comprehensive container security: from scanning images for vulnerabilities to runtime security monitoring and secrets protection.
Deployment Strategies: Rolling, Blue-Green, and Canary Releases
Compare and implement deployment strategies—rolling updates, blue-green deployments, and canary releases—to reduce risk and enable safe production releases.
Developing Helm Charts: Templates, Values, and Testing
Create production-ready Helm charts with Go templates, custom value schemas, and testing using Helm unittest and ct.