What maxSurge, maxUnavailable and minReadySeconds control

maxSurge and maxUnavailable sit under spec.strategy.rollingUpdate and cap how far the pod count may drift from spec.replicas during a rollout. maxSurge is how many pods may exist above the desired count, so it sets your temporary extra capacity and cost. maxUnavailable is how many pods may be missing below it, so it sets how much serving capacity you are willing to lose. Both accept an integer or a percentage of desired replicas. Both default to 25%. They cannot both be zero, because the controller would then be unable to add or remove any pod.

minReadySeconds is a top-level spec field and defaults to 0. A new pod counts as available only after it has stayed Ready for that many seconds with no container crashing. With the default, a pod counts the moment its readiness probe first passes. progressDeadlineSeconds, also under spec, sets how long the controller waits for progress before it marks the rollout as failed.

Percentages resolve differently for each field. Surge rounds up and unavailable rounds down, so the rounding always favours keeping capacity.

Replicas maxSurge (25%, ceil) maxUnavailable (25%, floor) Max total pods Min available
3 1 0 4 3
4 1 1 5 3
10 3 2 13 8
200 50 50 250 150

Illustrative calculation from the defaults.

Work these numbers out for your own replica count. At 3 replicas the defaults allow no unavailable pods, so the rollout moves one surge pod at a time. At 200 replicas the same 25% starts 50 pods at once and lets 50 go missing. That can saturate node capacity, image pulls or database connections.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout-api
spec:
  replicas: 10
  # A new pod must stay Ready for 15s with no container crashing
  # before it counts as available (default: 0).
  minReadySeconds: 15
  # Mark the rollout as failed if it makes no progress for 10 minutes
  # (default: 600).
  progressDeadlineSeconds: 600
  selector:
    matchLabels:
      app: checkout-api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      # Up to 25% extra pods above replicas; rounds up: ceil(2.5) = 3 -> max 13 pods
      maxSurge: 25%
      # Up to 25% of pods missing; rounds down: floor(2.5) = 2 -> min 8 available
      maxUnavailable: 25%
  template:
    metadata:
      labels:
        app: checkout-api
    spec:
      containers:
        - name: app
          image: registry.example.com/checkout-api:1.4.2
          ports:
            - containerPort: 8080
          # Readiness gates when the pod counts as Ready; minReadySeconds starts after this passes.
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            periodSeconds: 5

A Deployment at 10 replicas that sets maxSurge, maxUnavailable, minReadySeconds and progressDeadlineSeconds, with comments showing how the 25% defaults resolve to 3 surge pods and 2 unavailable pods.

How a rolling update progresses batch by batch

Each rollout creates a new ReplicaSet for the updated pod template. The Deployment controller then adjusts two replica counts, new up and old down, while holding two invariants:

  • Total pods never exceed replicas + maxSurge.
  • Available pods never drop below replicas - maxUnavailable.

A pod counts as available only after it has been Ready continuously for minReadySeconds. If readiness drops during that window, the timer restarts. The controller removes old pods only as new pods become available, so availability, not pod creation, sets the pace.

Illustrative rollout: 4 replicas, maxSurge 1, maxUnavailable 0

The limits here are 5 total pods and 4 available pods.

Step Old New (available) What gates the next step
0 4 0 Surge room: create 1 new pod
1 4 1 (0) New pod must be Ready for minReadySeconds
2 3 1 (1) Old pod removed, surge room reopens
3 3 2 (1) Wait again
… … … Repeat until done
End 0 4 (4) Old ReplicaSet scaled to 0

When a new pod fails

Suppose the pod created at step 1 crashes, or fails readiness, before minReadySeconds elapses. It never becomes available. Removing an old pod would leave 3 available, which breaks the limit of 4. Adding another new pod would make 6 total, which breaks the limit of 5. The rollout stalls with all four old pods still serving. Section 5 covers how to detect this state and roll back.

Old and new versions run together

At every intermediate step, both versions receive traffic. Schema changes, message formats, API contracts and cache entries must be readable by both versions. If they are not, a rolling update is the wrong strategy for that change.

            +-----------------------------------------------+
            |           Deployment controller               |
            |  total pods <= replicas + maxSurge       = 5  |
            |  avail pods >= replicas - maxUnavailable = 4  |
            +-----------------------------------------------+
                 | scale old down                | scale new up
                 | (only after a new pod         | (only while
                 |  becomes available)           |  total < 5)
                 v                               v
   +---------------------------+   +---------------------------+
   | old ReplicaSet (v1)       |   | new ReplicaSet (v2)       |
   | 4 -> 3 -> 2 -> 1 -> 0     |   | 0 -> 1 -> 2 -> 3 -> 4     |
   +---------------------------+   +---------------------------+
                 |                               |
                 |                               v
                 |                 +---------------------------+
                 |                 | new pod Ready?            |
                 |                 |  no  -> never available;  |
                 |                 |         rollout stalls    |
                 |                 |  yes -> stays Ready for   |
                 |                 |         minReadySeconds,  |
                 |                 |         then available    |
                 |                 +---------------------------+
                 |                               |
                 v                               v
   +-----------------------------------------------------------+
   |  Ready v1 and v2 pods serve traffic side by side          |
   |  schemas, messages, APIs, caches must work for both       |
   +-----------------------------------------------------------+

With 4 replicas, maxSurge 1 and maxUnavailable 0, the Deployment controller adds one v2 pod at a time and removes a v1 pod only after that v2 pod has stayed Ready for minReadySeconds, so a failing pod stalls the rollout while v1 and v2 serve traffic together.

Choosing a profile: who each setting suits and when to use it

Pick a profile based on spare capacity and how much exposure to a bad version you can tolerate.

Profile Settings Preconditions Fits Risks
Zero-downtime, full capacity maxUnavailable: 0, maxSurge: 1 or more Spare CPU and memory for the surge pods, plus quota headroom Latency-sensitive APIs that run near their replica floor If a surge pod stays Pending, the rollout stalls indefinitely. progressDeadlineSeconds only marks it failed and does not roll back.
Capacity-constrained maxSurge: 0, maxUnavailable: 1 or more Remaining replicas can absorb the load during each step Fixed-size clusters, tight quotas, large or GPU pods Capacity drops during the rollout, so a traffic spike hits fewer pods
Fast rollout Higher values for both, such as 25–50% Plenty of headroom and strong automated checks Stateless workers, internal services, teams with fast rollback Exposes a larger share of traffic to a bad version sooner

A higher maxSurge raises peak resource requests. On a full cluster, this can make the cluster autoscaler add nodes. A new node takes time to join, which slows the rollout, and you pay for it until it scales back down. Setting both values to 0 is rejected because the controller would have no way to make progress.

Headroom checklist

  • Node allocatable: Run kubectl describe node <name> and compare Allocatable with Allocated resources. The free amount must fit at least one surge pod's requests on a node the pod can schedule to.
  • ResourceQuota: Run kubectl describe resourcequota -n <namespace>. Surge pods count against the quota, so Used plus the surge requests must stay under Hard.
  • Cluster autoscaler: Confirm the node group's maximum size allows growth. Also confirm that the time to add a node fits within progressDeadlineSeconds.
#!/usr/bin/env bash
# Pre-rollout headroom check. Placeholder namespace and deployment names.
set -euo pipefail
NS=payments
DEPLOY=checkout-api

# 1. Current strategy and the per-pod requests each surge pod will need.
kubectl get deployment "$DEPLOY" -n "$NS" \
  -o jsonpath='{.spec.strategy.rollingUpdate}{"\n"}{.spec.template.spec.containers[*].resources.requests}{"\n"}'

# 2. Allocatable capacity per node (total schedulable, not free).
kubectl get nodes \
  -o custom-columns=NAME:.metadata.name,CPU:.status.allocatable.cpu,MEMORY:.status.allocatable.memory

# 3. Requests already committed on each node. Free = Allocatable - Requests.
kubectl describe nodes | grep -A 8 'Allocated resources'

# 4. Quota: Used + surge requests must stay under Hard.
kubectl describe resourcequota -n "$NS"

# 5. Cluster autoscaler status, if it publishes its default status ConfigMap.
kubectl get configmap cluster-autoscaler-status -n kube-system \
  -o jsonpath='{.data.status}' 2>/dev/null || echo 'no autoscaler status ConfigMap found'

# During the rollout: bound the wait so a stalled rollout
# (for example, maxUnavailable: 0 with a Pending surge pod) does not hang the pipeline.
kubectl rollout status deployment/"$DEPLOY" -n "$NS" --timeout=10m || {
  # Show Pending pods and their scheduling events, then roll back explicitly.
  # progressDeadlineSeconds alone does not roll back.
  kubectl get pods -n "$NS" --field-selector=status.phase=Pending
  kubectl get events -n "$NS" --field-selector=reason=FailedScheduling
  kubectl rollout undo deployment/"$DEPLOY" -n "$NS"
  exit 1
}

A shell script that checks node allocatable capacity, committed requests, ResourceQuota usage and cluster autoscaler status before a rollout, then puts a timeout on the rollout and rolls back if it stalls.

Readiness probes and minReadySeconds: setting values from data

minReadySeconds only means something if the readiness probe is honest. The controller counts a new Pod as available once it has been Ready for that long without any container crashing. If the probe returns 200 the moment the process binds a port, the wait adds delay and catches nothing.

Probe checklist

  • Readiness reflects serving ability, not liveness. It checks loaded config, open local resources and warm critical caches. Liveness only answers "should this container be restarted?"
  • Timing matches observed behavior. Set initialDelaySeconds near the fastest real startup. Pair a periodSeconds of 5–10 with a failureThreshold of 3 so one slow response doesn't eject the Pod.
  • Slow starters use a startupProbe. It holds off liveness and readiness until startup completes, so you don't need a large initial delay that also slows fast starts.
  • No shared downstream checks. If every Pod's readiness depends on the same database, one database blip marks the whole fleet unready and drops all traffic.
  • Not over-strict. A tight timeoutSeconds during GC pauses or a 100% success requirement causes flapping and rollouts that stall at progressDeadlineSeconds.

Deriving minReadySeconds

Base the value on how long it takes a bad Pod to fail. If cache warmup or connection-pool exhaustion appears about 40 seconds after a Pod starts taking traffic, a value of 60 gives a margin.

The cost grows linearly with the number of batches. Illustrative estimate: 10 batches × 60 s adds roughly 10 minutes to every deploy.

minReadySeconds also can't detect wrong responses or a rising error rate in a Pod that stays Ready. Catching those requires metrics-based analysis outside the Deployment controller.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout-api
spec:
  replicas: 10
  # Observed time-to-failure ~40s after taking traffic; 60s adds margin
  minReadySeconds: 60
  # Must exceed the full rollout time, which minReadySeconds lengthens
  progressDeadlineSeconds: 1200
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels: {app: checkout-api}
  template:
    metadata:
      labels: {app: checkout-api}
    spec:
      containers:
      - name: app
        image: registry.example.com/checkout-api:1.4.2
        ports: [{containerPort: 8080}]
        startupProbe:        # slow cache load: allow up to 30 x 5s = 150s
          httpGet: {path: /healthz/startup, port: 8080}
          periodSeconds: 5
          failureThreshold: 30
        readinessProbe:      # config loaded, local pool open, cache warm; no shared DB check
          httpGet: {path: /healthz/ready, port: 8080}
          periodSeconds: 5
          timeoutSeconds: 2  # tolerate short GC pauses
          failureThreshold: 3
        livenessProbe:       # only: is the process wedged and needs a restart?
          httpGet: {path: /healthz/live, port: 8080}
          periodSeconds: 10
          failureThreshold: 3

A Deployment that pairs a startupProbe, a serving-focused readinessProbe and a separate livenessProbe with a minReadySeconds value derived from observed time-to-failure.

Detecting failed rollouts and rolling back

Kubernetes has one built-in way to decide a rollout is stuck: progressDeadlineSeconds (600 by default). If the controller sees no progress within that window, such as a new Pod becoming available or an old one scaling down, it sets the Deployment's Progressing condition to status: False with reason ProgressDeadlineExceeded. A Pod that keeps failing its readiness probe or sits in CrashLoopBackOff produces this state.

That condition is the only consequence. The controller does not revert anything. It keeps the new ReplicaSet at whatever scale it reached and leaves the old Pods that are still running in place. Your maxUnavailable setting determines how much capacity you lose while it stays in that state.

In CI

Run kubectl rollout status deployment/<name> after applying the manifest. It blocks until the rollout completes. It exits non-zero if the progress deadline is exceeded. You can also add --timeout so a pipeline doesn't wait longer than its own budget. Gate the next step on that exit code, and on failure run kubectl rollout undo deployment/<name> explicitly. Add --to-revision=<n> to target a known-good revision rather than just the previous one.

Inspecting state

  • kubectl rollout history deployment/<name> lists revisions. Add --revision=<n> to see that revision's Pod template.
  • kubectl describe deployment <name> shows the conditions, the old and new ReplicaSets, and recent events.
  • kubectl get deployment <name> -o jsonpath='{.status.conditions}' gives machine-readable output for scripts.

The dangerous case is a team that believes rollback is automatic. A failed deploy is then reported as "done," and a mix of old and broken Pods serves traffic at reduced capacity until someone notices, often during the next incident.

Coordinating with PDBs, HPA and graceful shutdown

  • Don't expect a PodDisruptionBudget to throttle the rollout. The Deployment controller deletes old Pods directly and never calls the Eviction API, so a PDB has no effect on maxUnavailable. PDBs do apply to node drains, which use eviction. If a cluster upgrade drains nodes while your rollout is in progress, the PDB counts the not-yet-ready new Pods as unhealthy. A drain can then block until the rollout settles. Set the PDB with that overlap in mind.

  • Account for the HPA changing the denominator. Percentages for maxSurge and maxUnavailable are computed against the current desired replica count. maxSurge rounds up and maxUnavailable rounds down. When the HPA scales mid-rollout, the controller spreads the change proportionally across the old and new ReplicaSets, so batch sizes shift. A scale-down at 4 replicas can round maxUnavailable: 25% to zero.

  • Drain old Pods before the process exits. Removing a Pod from Service endpoints and sending SIGTERM happen concurrently. A short preStop sleep keeps the process serving while load balancers and kube-proxy stop routing to it. After that, the app's SIGTERM handler finishes in-flight requests. terminationGracePeriodSeconds must cover both phases, because the grace period starts before preStop runs. When it expires, the kubelet sends SIGKILL. The example below uses a 5-second preStop sleep with a 30-second grace period.

  • Keep changes compatible in both directions. Old and new versions run side by side during the rollout and again after a rollback. Schema migrations must be additive first and destructive later. API fields must be tolerated when they are missing. Producers and consumers must both handle old and new message formats.

When to avoid rolling updates, alternatives, and impact on cost and engineering time

When old and new versions cannot run side by side, switch the Deployment to Recreate. Kubernetes then terminates all old Pods before it creates any new ones, so downtime is guaranteed. Use it in these cases:

  • Singleton consumers. A queue consumer or scheduler that must never run twice.
  • Incompatible changes. Schema or protocol changes that cannot be made additive.
  • ReadWriteOnce volumes. The volume is attached to one node, so a surged Pod on another node cannot mount it.
Approach Built from Downtime Gating signal
RollingUpdate One Deployment None if probes are accurate Readiness, minReadySeconds
Recreate One Deployment Yes None
Blue-green Two Deployments, Service selector switch None Manual or scripted check
Canary / progressive delivery Multiple Deployments, weighted routing None Error-rate and latency metrics

Choose canary or progressive delivery when a Pod can pass its readiness probe and still serve bad responses.

Cost and time

Surge Pods, and any nodes the autoscaler adds to fit them, are billed capacity. A longer minReadySeconds makes every deploy take longer.

Illustrative estimate (assumed numbers, not a benchmark):

  • Assumed inputs: 20 replicas, maxSurge of 5, maxUnavailable of 0, 30 s to become ready, minReadySeconds of 30.
  • Batches = 20 ÷ 5 = 4.
  • Rollout time ≈ batches × (ready time + minReadySeconds) = 4 × 60 s = 4 minutes.
  • Extra capacity ≈ 5 Pods running for those 4 minutes.

These costs are small compared with what deliberate settings prevent: rollouts stuck at a quota limit, hours of incident triage, and manual rollbacks done under pressure.

Pre-deploy checklist

  • Compute the resolved integer values of maxSurge and maxUnavailable.
  • Confirm the cluster has headroom for the surge Pods.
  • Validate that readiness probes report what you think they report.
  • Set minReadySeconds from measured data.
  • Set progressDeadlineSeconds.
  • Alert on rollout failure.
  • Test rollout and rollback in staging at production replica counts.

How OneEnv can help

Disclosure: OneEnv is the product behind this blog.

OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.

Applied to kubernetes rolling update maxsurge, this is where it helps:

Saves time

  • Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
  • One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
  • Reviewers see the exact diff and the list of affected services up front, so approval is a decision rather than an investigation.

Saves cost

  • Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
  • Less duplicated configuration to maintain: one shared definition inherited by many services.
  • Per-service approve/reject means a bad shared value can be refused where it would break something, before it is live.
  • OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.

These are qualitative benefits, not measured results — how much you save depends on how many services share the value.

Want to try it? Start at oneenv.io.