How long a Kubernetes rollout actually takes, and why nobody can tell you

17 August 2026

Kubernetes Rollout & Probe Timing Restart loops, capacity dips and stuck drains. Runs in your browser.

Ask how long a deploy takes and you get a stopwatch answer. Ask how much capacity it removes, or how long a container gets to start before something kills it, and the honest answer is that the number is arithmetic across five fields in three different objects, and almost nobody has done it.

The fields are not near each other, the units are seconds in some places and counts in others, and every wrong answer shows up as a restart loop or a stuck drain rather than as an error.

The three numbers worth having

1. How long a container gets to start

A liveness probe kills a container after failureThreshold consecutive failures, starting at initialDelaySeconds and repeating every periodSeconds. The worst case is:

initialDelaySeconds + (periodSeconds x failureThreshold)

The defaults are initialDelaySeconds: 0, periodSeconds: 10, failureThreshold: 3, timeoutSeconds: 1. So a liveness probe written with no timings gives your container 30 seconds to start, and the first check happens immediately.

An application that needs 45 seconds to warm a cache, connect a pool and load a model restarts forever under that probe. The logs show a healthy startup every single time, because it is healthy: it is being killed 15 seconds before it finishes.

The tell is the regularity. A real crash varies. A probe-induced loop has identical intervals, because the probe executes the container at exactly the same point in its startup on every attempt.

2. How much capacity a rollout removes

maxUnavailable and maxSurge both default to 25%. The asymmetry that catches people is that maxUnavailable rounds down and maxSurge rounds up.

ReplicasmaxUnavailable (25%, rounds down)maxSurge (25%, rounds up)Minimum serving
2012
3013
4113
8226
10238

That rounding is a safety default and it is why small deployments feel slow: at 3 replicas the rollout can only proceed one surge pod at a time, and it cannot take anything down until a replacement is ready. It also means the percentage you wrote is not the percentage you get, and the difference is largest exactly where it matters most, at low replica counts.

The two values cannot both be zero. If you set maxUnavailable: 0 you are requiring surge capacity, and the rollout blocks if the cluster cannot schedule the extra pod. That is a correct choice for a service that must not lose capacity, and it turns a resource shortage from a slow rollout into a stopped one.

3. Whether a node drain can complete at all

A PodDisruptionBudget with minAvailable equal to the replica count permits zero voluntary disruption. kubectl drain will wait, forever, and report nothing that looks like an error. The same is true of maxUnavailable: 0.

spec:
  replicas: 3
---
spec:
  minAvailable: 3   # this blocks every drain, permanently

It is a one-character difference from minAvailable: 2, which permits one pod to move at a time, and it is written by someone reasoning about availability rather than about drains. Cluster upgrades are where it surfaces, usually at the worst moment, because that is the first time anybody drains a node.

How it shows up in production

The rollout that “hangs”. kubectl rollout status sits there. The new ReplicaSet has pods that never become ready, so the old one is never scaled down. progressDeadlineSeconds defaults to 600, so it takes ten minutes to report a failure, and by then someone has run kubectl rollout undo and the evidence is gone.

The deploy that drops requests. Users see connection resets during every deploy and nobody can reproduce it. This is the shutdown sequence rather than the rollout: pod deletion and endpoint removal are concurrent, not ordered. The kubelet sends SIGTERM at the same moment the endpoints controller starts removing the pod from the Service, and every kube-proxy has to catch up independently. Traffic arrives at a pod that is already shutting down for as long as that propagation takes.

The cluster upgrade that stops on node three. A drain that never completes, because of a PDB written a year ago by someone who has left.

The wrong instinct: raise initialDelaySeconds

The container is being killed during startup, so give it longer before the first check. It works, and it is the wrong fix, for two reasons.

It is a magic constant that has to be maintained forever. It is correct until startup gets slower, which it will: a larger dataset, a cold cache, a slow dependency, a noisy neighbour. Then it needs a new value, chosen by whoever is on call at the time.

It weakens liveness for the entire life of the pod. initialDelaySeconds delays the first check and nothing else, but the number people actually raise is failureThreshold, and that applies forever. A container given 30 failures to tolerate startup also gets 300 seconds of being wedged in production before anything acts.

The right fix is a startup probe, which exists precisely to separate the two questions. It runs first, holds liveness off until it passes once, and its generous failureThreshold costs nothing after startup:

startupProbe:
  httpGet: { path: /healthz, port: 8080 }
  failureThreshold: 30    # 30 x 10s = up to 5 minutes to start
  periodSeconds: 10
livenessProbe:
  httpGet: { path: /healthz, port: 8080 }
  periodSeconds: 10
  failureThreshold: 3     # 30 seconds to act on a wedged process

Startup can now vary by five minutes without anybody touching a number.

Two related things worth fixing at the same time:

  • successThreshold on a liveness or startup probe must be 1. Any other value is rejected by the API. It is only meaningful on readiness.
  • timeoutSeconds must be shorter than periodSeconds. The default timeout is 1 second, which is aggressive for anything doing real work in its health endpoint, and a timeout longer than the period means checks overlap.

Trade-offs in how fast to roll

StrategyCostWhen it is right
maxUnavailable: 0, maxSurge: 1Needs headroom for one extra pod. Slowest option: one pod at a time, and it stops entirely if the cluster cannot schedule the surgeAnything customer-facing. This is the correct default for a web service
maxUnavailable: 25%, maxSurge: 25%Serves at 75% capacity mid-rollout, so a rollout during peak is a self-inflicted capacity incidentInternal services, and anything with autoscaling headroom to spare
RecreateFull downtime, every deployA workload that genuinely cannot run two versions at once, usually a singleton holding an exclusive lock or a schema migration. Never a default

The cost of the safe option is real and worth stating: at 20 replicas with maxSurge: 1, a rollout is 20 sequential pod starts. If each takes 45 seconds to become ready, that is 15 minutes, plus minReadySeconds per pod if set. Deploy frequency and rollout safety trade directly against each other, and the lever is maxSurge, not the readiness probe.

What changed recently

Native sidecar containers are stable as of Kubernetes 1.33, having been on by default since 1.29. An init container with restartPolicy: Always starts before the main containers, stays running, and is terminated after them. This changes rollout timing directly: a service mesh proxy as a native sidecar no longer races the application on startup or dies before it during shutdown, which was a real cause of failed readiness at the start of a rollout and dropped connections at the end.

Probe-level terminationGracePeriodSeconds is GA as of 1.28. A probe can now specify how long the container gets after that probe fails, separately from the pod’s own grace period. A liveness failure on a wedged process no longer has to wait out a grace period sized for a clean drain.

progressDeadlineSeconds is 600 by default and is the field that decides how long a stuck rollout looks like a slow rollout. Lowering it to 300 on a service whose pods normally become ready in under a minute turns a ten minute mystery into a five minute failure with a reason attached.

Fixing a system already running

1. Do the probe arithmetic on your slowest service first. Multiply periodSeconds by failureThreshold, add initialDelaySeconds, and compare it against p99 startup time from your own logs rather than against a memory of how long it takes.

2. Add a startup probe before touching liveness. It is additive and safe: existing liveness behaviour is unchanged after startup succeeds.

3. Add a preStop sleep to anything behind a Service. Five to fifteen seconds, doing nothing:

lifecycle:
  preStop:
    exec: { command: ["/bin/sh", "-c", "sleep 10"] }
terminationGracePeriodSeconds: 45

The sleep covers the window where the pod is still receiving traffic while already being deleted. Then make sure terminationGracePeriodSeconds is longer than the sleep plus your actual drain time, because the grace period clock starts at deletion, not after the hook. A 10 second sleep under the default 30 second grace period leaves 20 seconds to finish in-flight work.

4. Audit every PDB against its workload’s replica count. minAvailable equal to replicas, or maxUnavailable: 0, blocks drains. This is a five minute audit that pays for itself the first time a cluster is upgraded.

5. Change one field per deploy. Probe timings and rollout parameters both affect how long a deploy takes, and changing them together makes a regression impossible to attribute.

When this is the wrong advice

A startup probe is wrong for a container that should start fast. If your service starts in two seconds and you give it a five minute startup budget, you have removed the signal that would have told you when a dependency made it slow. Size the budget to the real worst case, not to the largest number that stops the restarts.

maxUnavailable: 0 is wrong when you are resource constrained. It converts a capacity shortage into a rollout that never starts. On a cluster running near its node limit, maxUnavailable: 1 with maxSurge: 0 rolls in place and completes, and completing is worth more than the one pod of capacity.

A PodDisruptionBudget is wrong on a workload that can be interrupted. Batch jobs, queue consumers and anything with retry semantics do not need one, and a PDB on them is a drain that stalls for no benefit. The budget is for workloads where the disruption is visible to a user.

All of this is wrong for a StatefulSet. Ordered rollouts have their own rules: podManagementPolicy, partitioned updates, and per-pod ordering that the Deployment arithmetic above does not describe.

The short version

  • Default liveness timings give a container 30 seconds to start: 0 + 10 x 3.
  • maxUnavailable rounds down, maxSurge rounds up. At 3 replicas, 25% means 0 and 1.
  • minAvailable equal to the replica count blocks every drain, forever, with no error.
  • Pod deletion and endpoint removal are concurrent. Without a preStop sleep you drop in-flight requests on every deploy.
  • progressDeadlineSeconds is 600, so a stuck rollout looks slow for ten minutes before it looks failed.
  • Fix startup problems with a startup probe, not a bigger initialDelaySeconds and not a bigger failureThreshold.
  • Native sidecars are stable in 1.33 and remove a real class of rollout race.

The Kubernetes rollout and probe timing tool reads a manifest and does this arithmetic: how long each container gets before its liveness probe acts, how much capacity the rollout removes at your replica count, whether your PodDisruptionBudget permits a drain, and whether the grace period is long enough for the preStop hook in front of it. It runs entirely in your browser.