A Kubernetes rolling update replaces the pods of a Deployment a few at a time, so some pods are always serving while the new version rolls out. It's the default strategy for Deployments, and maxSurge and maxUnavailable control how fast it moves. A rolling update by itself doesn't guarantee zero downtime, though. You can still drop requests if pods receive traffic before they're ready, or if old pods die in the middle of a request.
This guide explains how the rollout works, then covers the three things that make it actually zero-downtime: readiness probes, graceful shutdown and a preStop hook. It also covers PodDisruptionBudgets and how to roll back with kubectl rollout.
How a Kubernetes rolling update works
When you change a Deployment's pod template, for example a new image tag, the Deployment controller creates a new ReplicaSet. It scales the new ReplicaSet up and the old one down in steps. Two settings bound each step:
maxSurge: how many pods above the desired count may exist during the update.maxUnavailable: how many pods below the desired count may be unavailable.
Both accept a number or a percentage, and both default to 25%. Percentages round up for maxSurge and down for maxUnavailable.
With replicas: 4 and the defaults, both work out to 1. The rollout can run at most 5 pods and must keep at least 3 available. In practice, the controller terminates one old pod and starts two new ones straight away (4 − 1 + 2 = 5). As new pods become ready, it removes more old pods and starts more new ones, staying within those bounds, until all 4 pods run the new version.
Common combinations:
| maxSurge | maxUnavailable | Behavior |
|---|---|---|
| 25% | 25% | Default. Balanced speed, with a small temporary capacity dip |
| 1 | 0 | Always full capacity. One extra pod at a time, slower |
| 100% | 0 | Brings up a complete new set, then removes the old one. Needs double the resources briefly |
| 0 | 1 | No extra pods. Useful when the cluster has no spare room, but capacity drops |
Both can't be 0, or the rollout could never make progress. For services where capacity matters, maxSurge: 1, maxUnavailable: 0 is a safe default.
The other strategy is Recreate. It stops every old pod before starting new ones, which means downtime. Use it only when two versions can't run side by side, for example an app that takes an exclusive lock on a volume. For StatefulSets and DaemonSets, which update differently, see Kubernetes workloads: Deployment, StatefulSet and DaemonSet.
Rolling updates alone aren't zero downtime
Setting maxUnavailable: 0 guarantees pod count. It doesn't guarantee every request succeeds. Three gaps remain.
1. Readiness probes: don't send traffic too early
A pod counts as available, and gets added to the Service's endpoints, when its readiness probe passes. Without a readiness probe, a pod is marked ready as soon as its containers start, which can be several seconds before the app has loaded config, warmed caches or connected to its database. Requests routed to it during that window fail.
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
periodSeconds: 5
failureThreshold: 3The ready endpoint should check what the pod needs in order to serve, and stay cheap. For apps that start slowly, add a startupProbe so the liveness probe doesn't kill them mid-boot. Kubernetes probes and CrashLoopBackOff covers probe design in detail.
minReadySeconds adds a further safety margin. A new pod must stay ready for that many seconds before the rollout counts it as available and moves on, which catches pods that pass once and then crash.
2. Graceful shutdown: finish in-flight requests
When Kubernetes removes an old pod:
- The pod is marked
Terminating. In parallel, it's removed from the Service's EndpointSlices, and thepreStophook runs if there is one. - After
preStopfinishes, the container receivesSIGTERM. - If the process is still running when
terminationGracePeriodSeconds(default 30) runs out, it getsSIGKILL. The grace period counts from step 1, so it includes thepreStoptime.
If your app exits immediately on SIGTERM, every request it's still processing fails. The app should stop accepting new connections, finish the requests in progress, then exit. In Go:
package main
import (
"context"
"errors"
"log"
"net/http"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz/ready", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello\n"))
})
srv := &http.Server{Addr: ":8080", Handler: mux}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer stop()
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen: %v", err)
}
}()
<-ctx.Done()
log.Println("SIGTERM received, draining")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown: %v", err)
}
}srv.Shutdown closes the listeners, closes idle keep-alive connections, and waits for active requests to finish. Background workers, consumers and database pools need the same treatment. Long-lived connections such as WebSockets or gRPC streams need an explicit close so clients reconnect to a new pod.
Also make sure the signal reaches your process. If a shell script is PID 1 and doesn't forward signals, your app never sees SIGTERM. Use exec in entrypoint scripts, or the exec form of CMD in the Dockerfile.
3. preStop hook: close the endpoint race
Step 1 above runs in parallel. Removing the pod from endpoints has to propagate to kube-proxy on every node, to Ingress controllers and to external load balancers, and that takes a moment. Meanwhile the pod may already be shutting down. The result is a short window where traffic still arrives at a pod that has stopped listening.
The fix is to delay shutdown slightly with a preStop hook, so routing updates first:
lifecycle:
preStop:
sleep:
seconds: 10The built-in sleep action is available in Kubernetes 1.30 and later. On older clusters, use exec with ["sleep", "10"], which needs a sleep binary in the image. Make sure terminationGracePeriodSeconds is larger than the preStop delay plus your app's shutdown timeout.
A zero-downtime Deployment manifest
Putting it together:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 4
revisionHistoryLimit: 10
progressDeadlineSeconds: 600
minReadySeconds: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
terminationGracePeriodSeconds: 45
containers:
- name: api
image: registry.example.com/api:1.9.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
periodSeconds: 5
failureThreshold: 3
lifecycle:
preStop:
sleep:
seconds: 10
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
memory: 256MiThe timing budget: 10 seconds of preStop plus up to 30 seconds of Shutdown is 40 seconds, inside the 45-second grace period.
One more requirement is outside Kubernetes: both versions run at the same time during a rollout. Database schema changes must work with the old and new code together (expand first, migrate, contract in a later release), and API changes must be backward compatible for clients that hit either version.
When a rollout gets stuck
If new pods never become ready, for example because of a bad image or a failing probe, the rollout stops making progress. It doesn't keep deleting old pods beyond what maxUnavailable allows, so the old version keeps serving. With maxUnavailable: 0 you keep full capacity. With the default 25%, you run at reduced capacity until it's fixed.
After progressDeadlineSeconds (default 600), the Deployment's Progressing condition becomes False with reason ProgressDeadlineExceeded. Kubernetes does not roll back automatically. Your pipeline has to notice and act:
kubectl rollout status deployment/api --timeout=10m || kubectl rollout undo deployment/apirollout status exits non-zero when the deadline is exceeded or the timeout hits, which makes it a good CI gate.
Rolling back with kubectl rollout
Each pod template change creates a new revision, and the Deployment keeps old ReplicaSets up to revisionHistoryLimit:
kubectl rollout history deployment/api # list revisions
kubectl rollout history deployment/api --revision=7 # show one revision's template
kubectl rollout undo deployment/api # back to the previous revision
kubectl rollout undo deployment/api --to-revision=7 # back to a specific revision
kubectl rollout pause deployment/api # batch several changes
kubectl rollout resume deployment/api
kubectl rollout restart deployment/api # rolling restart, same specTo record why each revision exists, set the kubernetes.io/change-cause annotation in your deploy step. The old --record flag is deprecated.
A rollback is also a rolling update, so the same strategy and probes apply. It only reverts the pod template. It doesn't revert ConfigMaps changed separately or database migrations. If you deploy through GitOps, revert the commit instead, or the next sync will re-apply the version you just rolled back.
PodDisruptionBudget: protecting against drains
A PodDisruptionBudget (PDB) limits how many pods of an app can be down at once due to voluntary disruptions: kubectl drain, node upgrades, and the Cluster Autoscaler removing a node.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api
spec:
maxUnavailable: 1
selector:
matchLabels:
app: api
unhealthyPodEvictionPolicy: AlwaysAllowWhat a PDB does not cover:
- Deployment rolling updates. Those are governed by
maxUnavailablein the strategy, not the PDB. - Involuntary failures, such as a node crashing or kernel OOM kills.
- Direct
kubectl delete pod.
Avoid PDBs that allow zero disruptions, such as minAvailable equal to the replica count. They block node drains and cluster scale-down indefinitely. unhealthyPodEvictionPolicy: AlwaysAllow stops pods that are already crash-looping from blocking a drain. Combine the PDB with spreading replicas across nodes, covered in Kubernetes requests, limits and scheduling, so a single drain never touches more than one replica.
FAQ
Is a Kubernetes rolling update zero downtime by default?
Not fully. It keeps pods running, but without a readiness probe, graceful SIGTERM handling and a short preStop delay, some requests fail during each rollout.
What are good values for maxSurge and maxUnavailable?
For user-facing services, maxSurge: 1 (or 25%) with maxUnavailable: 0 keeps full capacity. Raise maxSurge to go faster if you have spare cluster resources.
Does Kubernetes roll back a failed deployment automatically?
No. The rollout stalls and is marked ProgressDeadlineExceeded. Run kubectl rollout undo from your pipeline, or use a progressive delivery tool such as Argo Rollouts or Flagger for automated rollback.
Why do I get 502 errors during a rollout even with readiness probes?
Usually it's the endpoint removal race. The load balancer or Ingress still sends traffic to a terminating pod. Add a preStop sleep and make sure the app drains in-flight requests on SIGTERM.
Does a PodDisruptionBudget affect rolling updates?
No. Deployment rollouts follow the strategy's maxUnavailable. A PDB only limits evictions such as node drains and autoscaler scale-down.
Zero-downtime rollout checklist
maxSurge: 1,maxUnavailable: 0for critical services.- A readiness probe on every serving container, plus a
startupProbefor slow starters. - The app handles
SIGTERMby draining, and the signal actually reaches it. - A
preStopsleep of 5 to 10 seconds, withterminationGracePeriodSecondscovering sleep plus drain. - Schema and API changes compatible with both versions.
kubectl rollout statusas a CI gate, with a scriptedrollout undoor GitOps revert.- A PDB that allows at least one disruption, and replicas spread across nodes.
With these in place, deploying during business hours becomes routine. If you'd like to practise rollouts, rollbacks and drains on a real cluster, Vectorkub's free DevOps courses cover them.
