Zero-Downtime Deployments in Kubernetes: Rolling Updates vs Blue-Green vs Canary

Zero-Downtime Deployments in Kubernetes: Rolling Updates vs. Blue-Green vs. Canary

Deploying a new application version should not mean asking customers to tolerate failed requests, interrupted sessions, or unexpected service outages. Kubernetes provides rolling updates through its Deployment controller, but production-grade zero-downtime releases require more than changing a container image.

The real engineering challenge is coordinating application readiness, traffic routing, resource capacity, connection draining, database compatibility, and rollback behavior while old and new versions operate simultaneously.

Executive Summary

The problem: Traditional deployments can temporarily remove healthy application instances while replacing them, causing request failures when traffic exceeds the remaining capacity or new instances are not ready.

The solution: Use Kubernetes rolling updates for gradual replacement, blue-green deployments for controlled environment switching, or canary deployments for progressive traffic exposure, with health checks and tested rollback procedures protecting every release.

1. Understanding Zero-Downtime Deployments

Consider an e-commerce API running in a Kubernetes cluster. It receives approximately 1,500 requests per second across six application replicas. The team needs to release a new version containing a checkout optimization and an authentication fix.

A naive deployment might terminate the existing containers, download the new image, start replacement containers, and wait for the application to initialize. During that interval, the available replica count can drop significantly. If the remaining instances cannot absorb the incoming traffic, users experience increased latency, HTTP 502 responses, connection resets, or timeouts.

Kubernetes solves part of this problem by managing application replicas and gradually replacing old Pods with new ones. However, Kubernetes cannot automatically determine whether your application is compatible with its database, whether an existing request can finish safely, or whether the new version produces correct business results.

Rolling Updates vs Blue-Green vs Canary
  Rolling Updates vs Blue-Green vs Canary

What actually makes a deployment zero-downtime?

A deployment is operationally zero-downtime when users can continue using the service throughout a release without experiencing an unacceptable interruption. That requires several independent conditions:

  • Capacity: Enough healthy application instances remain available to serve incoming traffic.
  • Readiness: New containers receive production traffic only after initialization and dependency checks succeed.
  • Graceful termination: Existing instances stop accepting new requests while completing active requests.
  • Compatibility: Old and new application versions can safely operate against shared databases and APIs.
  • Traffic management: Load balancers stop routing traffic to instances that are shutting down or failing health checks.
  • Observability: Engineers can detect elevated error rates, latency, resource pressure, and business failures during rollout.
Production warning

A Kubernetes rollout reporting successfully rolled out does not prove that customers experienced zero errors. Deployment health and business-level health are different measurements.

Where downtime usually originates

In real systems, deployment failures often happen outside the container replacement mechanism. A new Pod can be healthy from Kubernetes' perspective but fail when it receives a checkout request because its database migration has not completed. A process can pass its readiness check while its connection pool is exhausted. A load balancer can also retain connections to an instance after it has started shutting down.

Stateful applications introduce additional problems. A worker might process the same queue message twice during termination. A database connection might be dropped during a long transaction. A background scheduler might execute the same scheduled job in both application versions.

For this reason, zero-downtime deployment is an application architecture concern as much as a Kubernetes configuration concern.

2. Rolling Updates vs. Blue-Green vs. Canary Deployments

These strategies solve related problems but offer different levels of traffic control, infrastructure overhead, and rollback complexity.

Evaluation Rolling Blue-Green Canary
Traffic migrationGradual Pod replacementSwitch between environmentsProgressive traffic weighting
Infrastructure costLow to moderateHigh during coexistenceModerate
Traffic controlLimitedEnvironment-levelFine-grained
RollbackDeployment revisionRoute back to old environmentReduce canary weight to zero
Release complexityLowModerateHigh
Best applicationMost stateless APIsHigh-risk releasesTraffic-sensitive systems

Rolling deployment

A rolling deployment replaces old Pods gradually. Kubernetes creates replacement Pods according to the Deployment strategy and removes old Pods as the new ones become available.

This is generally the most economical approach. It does not require maintaining two complete application environments, and the Deployment controller handles the replacement lifecycle.

The limitation is traffic isolation. During the update, users can be served by both versions. If version compatibility is not maintained, some requests may behave differently depending on which Pod receives them.

Blue-green deployment

Blue-green deployments maintain two application environments. Blue represents the currently active release, while green represents the candidate release.

The green environment is deployed and validated independently. Once verification succeeds, the Service or gateway is changed to direct traffic to green. Blue remains available temporarily for rollback.

This approach makes traffic switching straightforward, but it can nearly double application compute requirements during coexistence. It also does not automatically solve shared-database compatibility or state consistency.

Canary deployment

A canary deployment sends a small portion of production traffic to a new application version while the majority continues using the stable release.

For example, the initial traffic distribution might be 95% stable and 5% canary. After examining error rates, latency, resource utilization, and business metrics, the team can increase the canary percentage to 20%, 50%, and eventually 100%.

Canary releases are particularly useful for applications where synthetic testing cannot reproduce production traffic patterns. Their weakness is operational complexity: traffic weighting, metrics collection, automated analysis, and rollback decisions must work together.

Engineering recommendation

Start with rolling updates for ordinary stateless services. Introduce blue-green for releases that need an independent validation environment. Use canary releases when production traffic itself is needed to evaluate risk.

3. How Kubernetes Rolling Updates Work

A Kubernetes Deployment manages ReplicaSets, and each ReplicaSet manages a set of Pods. When the Pod template changes, such as when the container image changes, the Deployment controller creates a new ReplicaSet.

The controller gradually scales up the new ReplicaSet and scales down the previous ReplicaSet. It monitors readiness and availability while working toward the requested replica count.

Understanding maxSurge and maxUnavailable

Two parameters control the replacement process:

  • maxSurge: The maximum number of additional Pods that can exist above the desired replica count during an update.
  • maxUnavailable: The maximum number of Pods that can be unavailable during the update compared with the desired replica count.

Both values accept absolute numbers or percentages. Percentage values are rounded according to Kubernetes rules: maxSurge rounds up, while maxUnavailable rounds down for Deployment rolling updates.

Consider a Deployment with six replicas:

ConfigurationmaxSurgemaxUnavailableBehavior
Balanced21Up to 8 Pods, one unavailable
Capacity-first20Preserves desired availability
Cost-first01Replaces without surge capacity
Conservative10One additional Pod at a time

With six desired replicas, a 25% maxSurge permits two extra Pods because the percentage rounds up. A 25% maxUnavailable permits one unavailable Pod because that percentage rounds down.

Capacity calculation before deployment

Assume each application Pod requests 500 millicores of CPU and 512 MiB of memory. Six replicas therefore request 3 CPU cores and 3 GiB of memory.

With maxSurge set to 2, the rollout may temporarily require resources for eight Pods: 4 CPU cores and 4 GiB of requested memory. Actual consumption can be higher because of startup spikes, cache warming, and application-specific allocation behavior.

If the cluster has insufficient schedulable capacity, the new Pods remain Pending. The Deployment controller cannot complete the rollout until the required Pods can be scheduled and become available.

Capacity caveat

maxSurge is not a capacity reservation. It only controls the desired rollout behavior. Your scheduler still needs available CPU, memory, storage, networking, and suitable nodes to place the additional Pods.

4. Production Implementation: Kubernetes Rolling Updates

The following implementation uses a stateless Node.js HTTP API called checkout-api. It assumes that the application exposes /health/live and /health/ready endpoints and listens on port 3000.

The readiness endpoint must return success only after the application can safely process real requests. It should not simply return HTTP 200 because the Node.js process exists.

STEP 1: Create the application Deployment

STEP 1

Create checkout-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout-api
  namespace: production
  labels:
    app: checkout-api
spec:
  replicas: 6
  revisionHistoryLimit: 10
  minReadySeconds: 10
  progressDeadlineSeconds: 600

  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2
      maxUnavailable: 1

  selector:
    matchLabels:
      app: checkout-api

  template:
    metadata:
      labels:
        app: checkout-api
    spec:
      terminationGracePeriodSeconds: 45
      automountServiceAccountToken: false

      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        seccompProfile:
          type: RuntimeDefault

      containers:
        - name: checkout-api
          image: registry.example.com/checkout-api:1.4.0
          imagePullPolicy: IfNotPresent

          ports:
            - name: http
              containerPort: 3000
              protocol: TCP

          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 768Mi

          startupProbe:
            httpGet:
              path: /health/live
              port: http
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 30

          readinessProbe:
            httpGet:
              path: /health/ready
              port: http
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 3
            successThreshold: 1

          livenessProbe:
            httpGet:
              path: /health/live
              port: http
            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 3

          lifecycle:
            preStop:
              exec:
                command:
                  - /bin/sh
                  - -c
                  - sleep 5

          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop:
                - ALL
            runAsNonRoot: true

          volumeMounts:
            - name: tmp
              mountPath: /tmp

      volumes:
        - name: tmp
          emptyDir:
            sizeLimit: 64Mi

Engineering explanation:

  1. maxSurge: 2 permits two additional Pods, while maxUnavailable: 1 allows one unavailable replica during the rollout. This balances deployment speed and capacity.
  2. The startup probe protects slow initialization. Once it succeeds, Kubernetes starts applying readiness and liveness checks. Readiness controls traffic eligibility; liveness controls container restart behavior.
  3. The 45-second termination grace period provides time for shutdown. The preStop delay is illustrative; the application must independently stop accepting new work and drain active connections.

STEP 2: Create the Kubernetes Service

STEP 2

Create checkout-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: checkout-service
  namespace: production
spec:
  type: ClusterIP
  selector:
    app: checkout-api
  ports:
    - name: http
      port: 80
      targetPort: http
      protocol: TCP

Engineering explanation:

  1. The Service selects Pods using stable application labels rather than individual Pod names or IP addresses.
  2. When Kubernetes updates the Deployment, the Service continues targeting matching ready endpoints. This allows old and new replicas to coexist during the rollout.

STEP 3: Create a PodDisruptionBudget

STEP 3

Create checkout-pdb.yaml:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: checkout-api-pdb
  namespace: production
spec:
  minAvailable: 5
  unhealthyPodEvictionPolicy: AlwaysAllow
  selector:
    matchLabels:
      app: checkout-api

Engineering explanation:

  1. The budget requires five healthy Pods out of the six desired replicas for supported voluntary evictions.
  2. A PDB protects against certain voluntary disruptions, such as eviction during node maintenance. It does not replace Deployment rolling-update settings or guarantee protection against node failures.
Important interaction

Deployment rolling-update behavior is configured through maxSurge and maxUnavailable. PDBs primarily govern eviction requests. Do not assume that setting a PDB makes a Deployment rollout respect an identical availability limit.

STEP 4: Apply the configuration

kubectl create namespace production

kubectl apply -f checkout-deployment.yaml
kubectl apply -f checkout-service.yaml
kubectl apply -f checkout-pdb.yaml

kubectl rollout status deployment/checkout-api \
  -n production \
  --timeout=10m

kubectl get deployments,pods,services,pdb \
  -n production

Engineering explanation:

  1. kubectl apply creates or updates the declarative resources. Applying the namespace first avoids a namespace-not-found error.
  2. kubectl rollout status waits for the Deployment controller to complete its rollout, subject to the specified timeout. It does not measure customer-facing error rates.

STEP 5: Release a new application version

STEP 5
kubectl set image deployment/checkout-api \
  checkout-api=registry.example.com/checkout-api:1.5.0 \
  -n production

kubectl rollout status deployment/checkout-api \
  -n production \
  --timeout=10m

kubectl get rs -n production

kubectl get pods \
  -n production \
  -l app=checkout-api \
  -o wide

Engineering explanation:

  1. Changing the image updates the Deployment Pod template and triggers a new revision and ReplicaSet.
  2. The controller creates replacement Pods, waits for their availability, and progressively reduces the old ReplicaSet.

5. Implementing Blue-Green Deployments in Kubernetes

Blue-green deployments are useful when the engineering team wants to validate an entire candidate environment before sending production traffic to it.

Unlike a normal rolling update, the blue and green versions are separate Deployments with independent ReplicaSets. A stable Kubernetes Service selects whichever environment is active.

Architecture

                 Production Traffic
                         |
                         v
                checkout-service
                         |
                 Selector: track=blue
                         |
             +-----------+-----------+
             |                       |
             v                       v
        BLUE ENVIRONMENT        GREEN ENVIRONMENT
        Version 1.4.0           Version 1.5.0
        6 replicas              6 replicas
        ACTIVE                  VALIDATION ONLY

             Traffic switch:
             track=blue
                  |
                  v
             track=green

STEP 1: Deploy the blue environment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout-blue
  namespace: production
spec:
  replicas: 6
  selector:
    matchLabels:
      app: checkout
      track: blue
  template:
    metadata:
      labels:
        app: checkout
        track: blue
    spec:
      containers:
        - name: checkout-api
          image: registry.example.com/checkout-api:1.4.0
          ports:
            - containerPort: 3000
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 3000
            periodSeconds: 5
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 768Mi

Engineering explanation:

  1. The track: blue label identifies the stable environment. The Deployment selector must match the Pod template labels.
  2. Blue is independent of the green Deployment, so replacing green Pods does not directly scale down the active blue ReplicaSet.

STEP 2: Deploy the green environment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout-green
  namespace: production
spec:
  replicas: 6
  selector:
    matchLabels:
      app: checkout
      track: green
  template:
    metadata:
      labels:
        app: checkout
        track: green
    spec:
      containers:
        - name: checkout-api
          image: registry.example.com/checkout-api:1.5.0
          ports:
            - containerPort: 3000
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 3000
            periodSeconds: 5
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 768Mi

Engineering explanation:

  1. Green starts six independent replicas. This doubles the application Pod count during coexistence, excluding infrastructure and monitoring overhead.
  2. Validate green through a separate internal test Service or controlled test route before changing production traffic.

STEP 3: Configure the production Service

apiVersion: v1
kind: Service
metadata:
  name: checkout-service
  namespace: production
spec:
  selector:
    app: checkout
    track: blue
  ports:
    - name: http
      port: 80
      targetPort: 3000

Engineering explanation:

  1. The production Service initially routes traffic only to blue Pods that are ready and match both labels.
  2. The Service remains stable while its selector changes. Clients do not need a new Service address.

STEP 4: Switch production traffic to green

kubectl patch service checkout-service \
  -n production \
  --type=merge \
  -p '{"spec":{"selector":{"app":"checkout","track":"green"}}}'

kubectl get endpointslice \
  -n production \
  -l kubernetes.io/service-name=checkout-service

kubectl get pods \
  -n production \
  -l app=checkout,track=green

Engineering explanation:

  1. The patch changes the selector to green. EndpointSlice updates are asynchronous, so the switch is not a perfectly instantaneous transaction across every proxy and established connection.
  2. Keep blue running until application-level verification and the defined rollback window have passed.

STEP 5: Roll back to blue if green fails

kubectl patch service checkout-service \
  -n production \
  --type=merge \
  -p '{"spec":{"selector":{"app":"checkout","track":"blue"}}}'

kubectl get endpointslice \
  -n production \
  -l kubernetes.io/service-name=checkout-service

Engineering explanation:

  1. Rollback changes the active Service selector. It does not rebuild or redeploy blue.
  2. Existing keep-alive connections may continue using the previous backend temporarily, depending on the network proxy and connection lifecycle. Verify actual traffic rather than assuming that a selector patch instantly moves every connection.
Database compatibility warning

If green applies a destructive database migration, switching traffic back to blue may not restore compatibility. Use backward-compatible expand-and-contract migrations and preserve old-schema compatibility throughout the rollback window.

6. Implementing Canary Deployments with Kubernetes

A canary release is not simply running two Deployments simultaneously. The critical requirement is controlling how much production traffic reaches each version.

Kubernetes Services distribute traffic among matching endpoints, but a standard Service does not provide an application-level percentage such as 95% stable and 5% canary. For weighted traffic routing, use a gateway, service mesh, or progressive-delivery controller.

The following example uses the Kubernetes Gateway API HTTPRoute resource. It assumes a compatible Gateway API CRD and controller are installed and configured.

STEP 1: Create stable and canary Deployments

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout-stable
  namespace: production
spec:
  replicas: 6
  selector:
    matchLabels:
      app: checkout-canary-demo
      track: stable
  template:
    metadata:
      labels:
        app: checkout-canary-demo
        track: stable
    spec:
      containers:
        - name: checkout-api
          image: registry.example.com/checkout-api:1.4.0
          ports:
            - containerPort: 3000
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 3000
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 768Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout-canary
  namespace: production
spec:
  replicas: 1
  selector:
    matchLabels:
      app: checkout-canary-demo
      track: canary
  template:
    metadata:
      labels:
        app: checkout-canary-demo
        track: canary
    spec:
      containers:
        - name: checkout-api
          image: registry.example.com/checkout-api:1.5.0
          ports:
            - containerPort: 3000
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 3000
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 768Mi

Engineering explanation:

  1. Stable and canary have different selectors, so they can scale independently and receive separate Service traffic.
  2. One canary replica is a starting point, not a universal capacity recommendation. At high request volumes, one instance may be overloaded even with a small traffic percentage.

STEP 2: Create separate Services

apiVersion: v1
kind: Service
metadata:
  name: checkout-stable
  namespace: production
spec:
  selector:
    app: checkout-canary-demo
    track: stable
  ports:
    - name: http
      port: 80
      targetPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: checkout-canary
  namespace: production
spec:
  selector:
    app: checkout-canary-demo
    track: canary
  ports:
    - name: http
      port: 80
      targetPort: 3000

Engineering explanation:

  1. Each Service exposes only its matching release. This provides independent backends for the gateway.
  2. Readiness determines which Pods become eligible endpoints. A failing canary should not receive traffic simply because its Deployment exists.

STEP 3: Configure weighted traffic using HTTPRoute

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: checkout-route
  namespace: production
spec:
  parentRefs:
    - name: production-gateway
      namespace: production
  hostnames:
    - checkout.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: checkout-stable
          port: 80
          weight: 95
        - name: checkout-canary
          port: 80
          weight: 5

Engineering explanation:

  1. The backend weights represent a 95:5 distribution for traffic handled by the configured Gateway implementation. They are relative weights, not a guarantee of exactly 5% in every short observation window.
  2. The gateway controller must support the relevant HTTPRoute functionality. Validate the route status and controller-specific behavior before relying on it in production.

STEP 4: Increase canary exposure gradually

A practical release schedule could look like this:

StageStableCanaryDecision
Initial95%5%Check basic health
Stage 280%20%Compare latency and errors
Stage 350%50%Evaluate sustained traffic
Final0%100%Promote release

The duration of each stage should depend on traffic volume, business risk, and the time required to observe meaningful behavior. A five-minute test is not sufficient for a low-volume service that processes only a few dozen requests during that interval.

STEP 5: Abort the canary

kubectl patch httproute checkout-route \
  -n production \
  --type=merge \
  -p '{"spec":{"rules":[{"matches":[{"path":{"type":"PathPrefix","value":"/"}}],"backendRefs":[{"name":"checkout-stable","port":80,"weight":100},{"name":"checkout-canary","port":80,"weight":0}]}]}}'

kubectl scale deployment/checkout-canary \
  -n production \
  --replicas=0

kubectl get httproute checkout-route \
  -n production

Engineering explanation:

  1. The route is changed to send traffic entirely to stable. Confirm that the gateway has accepted the new configuration before terminating canary capacity.
  2. Scaling canary to zero is a cleanup action, not the traffic rollback itself. Keep the stable Deployment untouched during the recovery.
Canary measurement rule

Compare stable and canary using equivalent request populations, regions, time windows, and endpoint types. A canary receiving mostly inexpensive health checks is not being meaningfully tested against production workload behavior.

7. Rollback Procedures and Release Verification

Rollback should be a documented operational procedure, not a command that someone searches for after production starts failing.

Kubernetes stores Deployment revision history when the Pod template changes. Engineers can inspect revisions, compare rollout history, and restore a previous Pod template.

Inspect Deployment history

kubectl rollout history deployment/checkout-api \
  -n production

kubectl rollout history deployment/checkout-api \
  -n production \
  --revision=2

kubectl get rs \
  -n production \
  -l app=checkout-api

Explanation:

  1. Rollout history displays recorded Deployment revisions. Revisions are created when the Pod template changes, not for every ordinary scaling change.
  2. Review the previous image, environment references, and Pod configuration before deciding which revision is safe to restore.

Rollback to the previous revision

kubectl rollout undo deployment/checkout-api \
  -n production

kubectl rollout status deployment/checkout-api \
  -n production \
  --timeout=10m

kubectl get pods \
  -n production \
  -l app=checkout-api

Explanation:

  1. rollout undo restores the previous recorded Pod template. It does not reverse database migrations, external configuration changes, or third-party side effects.
  2. Verify the restored Pods, application metrics, and actual user-facing requests before declaring recovery complete.

Rollback to a specific revision

kubectl rollout undo deployment/checkout-api \
  -n production \
  --to-revision=2

Use this only after verifying that revision 2 is the intended release. Revision numbers are deployment-specific and change as new releases occur.

Pause a rollout during investigation

kubectl rollout pause deployment/checkout-api \
  -n production

kubectl rollout status deployment/checkout-api \
  -n production

kubectl rollout resume deployment/checkout-api \
  -n production

Pausing a Deployment stops further progression of the rollout. It does not automatically restore old Pods or cancel requests already reaching the new version.

8. Terminal Output and Production Verification

After every deployment, verify the state of the Deployment, ReplicaSets, Pods, Service endpoints, and application responses. Do not rely on a single successful command.

Check rollout progress

$ kubectl rollout status deployment/checkout-api -n production

Waiting for deployment "checkout-api" rollout to finish:
2 out of 6 new replicas have been updated...
4 out of 6 new replicas have been updated...
6 out of 6 new replicas have been updated...
deployment "checkout-api" successfully rolled out

Interpretation: This is an illustrative expected output. It indicates that Kubernetes considers the Deployment rollout complete. It does not prove that every request succeeded.

Inspect Pod readiness

$ kubectl get pods -n production -l app=checkout-api

NAME                            READY   STATUS    RESTARTS
checkout-api-7f8c6d9b4c-2k8px   1/1     Running   0
checkout-api-7f8c6d9b4c-5n9qw   1/1     Running   0
checkout-api-7f8c6d9b4c-8m2rt   1/1     Running   0
checkout-api-7f8c6d9b4c-9v6jp   1/1     Running   0
checkout-api-7f8c6d9b4c-b7w2x   1/1     Running   0
checkout-api-7f8c6d9b4c-q4k6z   1/1     Running   0

Inspect Service endpoints

$ kubectl get endpointslice -n production \
  -l kubernetes.io/service-name=checkout-service

NAME                    ADDRESSTYPE   PORTS   ENDPOINTS
checkout-service-abc12  IPv4          3000    6

Test the application from inside the cluster

$ kubectl run checkout-test \
  -n production \
  --rm -i \
  --restart=Never \
  --image=curlimages/curl:8.12.1 \
  --command -- \
  curl --fail --show-error --silent \
  http://checkout-service/health/ready

{"status":"ready","version":"1.5.0"}

The test image must be available from your approved registry, and your cluster policy must permit this temporary diagnostic Pod. In a restricted production environment, use an existing diagnostic workload instead.

Measure actual application performance

A mature deployment pipeline should compare the following metrics before promoting a release:

  • HTTP 5xx error rate and total request count.
  • Request latency at p50, p95, and p99.
  • CPU throttling and memory working-set utilization.
  • Database connection pool saturation and query latency.
  • Queue processing lag and retry counts.
  • Business transaction success rate, such as completed checkouts.

Example Prometheus queries for an application exposing standard HTTP request metrics:

# Request error ratio
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
clamp_min(sum(rate(http_requests_total[5m])), 0.001)

# 95th percentile latency
histogram_quantile(
  0.95,
  sum by (le) (
    rate(http_request_duration_seconds_bucket[5m])
  )
)

# Container memory working set
container_memory_working_set_bytes{
  namespace="production",
  pod=~"checkout-api-.*"
}

# CPU throttling ratio
sum(rate(container_cpu_cfs_throttled_periods_total{
  namespace="production"
}[5m]))
/
clamp_min(
  sum(rate(container_cpu_cfs_periods_total{
    namespace="production"
  }[5m])),
  0.001
)

These expressions assume compatible metric names and labels. Validate the actual instrumentation exposed by your Prometheus environment before copying them into an alerting rule.

9. Common Production Pitfalls and Troubleshooting

Production Troubleshooting Alerts

Problem 1: New Pods remain Pending

Typical output:

Warning  FailedScheduling
0/3 nodes are available:
Insufficient cpu.

Root cause: The scheduler cannot find a node with enough available resources for the surge Pods. Node selectors, affinity rules, taints, and resource requests can also prevent placement.

Fix: Check node capacity and Pod requests, review scheduling constraints, add capacity or reduce rollout surge. Do not reduce resource requests below realistic workload requirements just to force scheduling.

kubectl describe pod <pending-pod> -n production
kubectl top nodes
kubectl describe nodes

Problem 2: Rollout stuck because readiness fails

Typical output:

Readiness probe failed:
HTTP probe failed with statuscode: 503

Root cause: The application is running but cannot serve requests. It may still be warming caches, loading configuration, connecting to a database, or waiting for an upstream dependency.

Fix: Inspect application logs, correct the readiness endpoint, and configure startupProbe for slow initialization. Increase the progress deadline only when the expected initialization duration justifies it.

kubectl describe pod <pod-name> -n production
kubectl logs <pod-name> -n production
kubectl get events -n production --sort-by=.lastTimestamp

Problem 3: New containers repeatedly restart

Typical output:

State: Waiting
Reason: CrashLoopBackOff

Last State:
Reason: OOMKilled
Exit Code: 137

Root cause: The process may exceed its memory limit, encounter an unhandled startup error, or enter a crash loop caused by a bad dependency configuration.

Fix: Inspect previous-container logs, memory working set, heap usage, and resource requests. Fix the memory leak or right-size the container based on measurements.

kubectl logs <pod-name> -n production --previous
kubectl describe pod <pod-name> -n production
kubectl top pods -n production

Problem 4: Application returns errors during termination

Typical output:

ECONNRESET
socket hang up
upstream prematurely closed connection

Root cause: The process is terminated before existing requests finish. The application may not handle SIGTERM, or the load balancer may still have active connections.

Fix: Implement graceful shutdown, stop accepting new connections, drain active requests, and set terminationGracePeriodSeconds to exceed the expected drain time.

10. Forensic Failure Ledger: Four Complex Deployment Incidents

The following incident records are representative engineering failure scenarios. Their traces are illustrative synthetic examples, not claims about a particular company's production outage. They show how deployment mechanics interact with real application failure modes.

Incident 01: Connection Leak During Network Degradation

Severity: High | Failure category: Resource exhaustion

Observed error trace

Error: connect ETIMEDOUT 10.0.4.21:5432
    at TCPConnectWrap.afterConnect
    at PoolConnection.connect
    at CheckoutRepository.findOrder
    at CheckoutController.getOrder

Error: too many clients already
SQLSTATE: 53300

Root cause analysis

During a rolling deployment, old and new application replicas operated simultaneously. Both versions created database connection pools sized for normal replica counts. At the same time, network degradation increased connection establishment time and caused retry attempts.

The application failed to release some connections after timed-out requests. The database reached its connection limit, affecting both old and new versions. A rollout intended to improve the application instead exhausted a shared dependency.

Production patch

Use bounded connection pools, finite connection acquisition timeouts, and explicit cleanup. The following Node.js example uses the PostgreSQL pg library.

import pg from "pg";

const { Pool } = pg;

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
  connectionTimeoutMillis: 3000,
  idleTimeoutMillis: 30000,
  statement_timeout: 5000,
  query_timeout: 6000,
  application_name: "checkout-api"
});

export async function findOrder(orderId) {
  const client = await pool.connect();

  try {
    const result = await client.query(
      "SELECT id, status FROM orders WHERE id = $1",
      [orderId]
    );

    return result.rows[0] ?? null;
  } finally {
    client.release();
  }
}

export async function closeDatabase() {
  await pool.end();
}

Patch analysis:

  • The connection pool limits the maximum number of connections per application replica. Calculate the cluster-wide total, including old and new replicas.
  • The acquisition and query timeouts bound waiting time. The finally block returns acquired clients to the pool.
  • Retries should be bounded, jittered, and limited to operations safe to retry. Never retry non-idempotent payment operations blindly.

Deployment correction: Include connection-pool capacity in the maxSurge calculation. Six replicas with a pool size of 10 can use up to 60 connections; eight replicas can use up to 80, before other workloads are counted.

Incident 02: Lock Contention and Duplicate Job Processing

Severity: Critical | Failure category: Concurrency and race condition

Observed error trace

ERROR: deadlock detected
SQLSTATE: 40P01

Process 4812 waits for ShareLock
on transaction 72913

Process 4812:
UPDATE inventory SET quantity = quantity - 1

Process 4821:
UPDATE orders SET status = 'CONFIRMED'

Root cause analysis

Both old and new worker versions were running during the rollout. Each processed the same logical business operation through different execution paths. One transaction locked inventory before orders, while another locked orders before inventory.

The inconsistent lock order created a deadlock. In addition, retries were not protected by an idempotency key, so some messages were processed more than once.

Production patch

Use a stable transaction lock order and a database uniqueness constraint to enforce idempotency. This PostgreSQL migration creates a record of processed business events.

CREATE TABLE IF NOT EXISTS processed_events (
    event_id UUID PRIMARY KEY,
    processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS
idx_processed_events_processed_at
ON processed_events (processed_at);

The worker must insert the event identifier and perform the associated business update in the same transaction.

import pg from "pg";

const { Pool } = pg;
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10
});

export async function processInventoryEvent(event) {
  const client = await pool.connect();

  try {
    await client.query("BEGIN");

    const inserted = await client.query(
      `INSERT INTO processed_events (event_id)
       VALUES ($1)
       ON CONFLICT (event_id) DO NOTHING
       RETURNING event_id`,
      [event.id]
    );

    if (inserted.rowCount === 0) {
      await client.query("ROLLBACK");
      return { processed: false, reason: "duplicate" };
    }

    await client.query(
      `UPDATE inventory
       SET quantity = quantity - $1
       WHERE product_id = $2
         AND quantity >= $1`,
      [event.quantity, event.productId]
    );

    await client.query(
      `UPDATE orders
       SET status = 'CONFIRMED'
       WHERE id = $1`,
      [event.orderId]
    );

    await client.query("COMMIT");

    return { processed: true };
  } catch (error) {
    await client.query("ROLLBACK");
    throw error;
  } finally {
    client.release();
  }
}

Patch analysis:

  • Every worker uses the same event identifier, so duplicate deliveries cannot create a second successful processing record.
  • Keep database lock acquisition order consistent across application versions. Add bounded retries for retryable transaction failures such as deadlocks.
  • For high-volume systems, consider a transactional outbox and explicit queue visibility or acknowledgement semantics.

Deployment correction: Ensure that old and new workers agree on message schemas and idempotency semantics. A Kubernetes rollout does not coordinate application-level job ownership automatically.

Incident 03: Memory Thrashing During Sustained Traffic

Severity: High | Failure category: Memory pressure

Observed error trace

FATAL ERROR: Reached heap limit
Allocation failed - JavaScript heap out of memory

Container status:
Reason: OOMKilled
Exit Code: 137

Deployment:
ProgressDeadlineExceeded

Root cause analysis

The new version introduced a cache that retained response objects without a bounded eviction policy. Memory consumption increased as traffic continued. The new Pods were terminated by the container memory limit, while the old Pods handled additional requests and also experienced increased pressure.

Increasing maxSurge made the problem worse because more copies of the memory-heavy application could run simultaneously.

Production patch

Bound application-level caches and measure heap usage separately from total container memory. The following Node.js example uses an LRU cache with a maximum entry count and a time-to-live.

import { LRUCache } from "lru-cache";

export const responseCache = new LRUCache({
  max: 5000,
  ttl: 60_000,
  updateAgeOnGet: false,
  allowStale: false
});

export function getCachedResponse(key) {
  return responseCache.get(key);
}

export function setCachedResponse(key, value) {
  responseCache.set(key, value);
}

Patch analysis:

  • The maximum entry count prevents unlimited key growth. Select the value using measured average entry size and expected traffic.
  • TTL limits retention duration. A short TTL alone is not sufficient if the arrival rate is higher than the expiration rate.
  • Node.js heap limits do not account for every native allocation or external memory allocation. Monitor container memory working set and process memory together.

For example, a 512 MiB container limit does not mean that Node.js should use a 512 MiB heap. The process also needs space for native libraries, buffers, sockets, runtime overhead, and temporary allocations.

Deployment correction: Test the new version under sustained representative load before increasing rollout speed. Use measured peak memory per Pod and cluster headroom to select resource limits and maxSurge.

Incident 04: Split-Brain and Stale Environment Routing

Severity: Critical | Failure category: State synchronization

Observed error trace

ERROR: duplicate payment operation
HTTP 409 Conflict

ERROR: stale configuration version
Expected: 42
Received: 41

Gateway:
Backend health check passed
Application transaction failed

Root cause analysis

A blue-green release changed the active Service selector, but an old background worker continued processing messages. Blue and green used different cache versions and configuration snapshots. Both environments could perform the same business operation because they did not share an effective idempotency boundary.

Some requests also continued over existing connections to the previous backend after the route switch. The engineering team assumed that changing the Service selector was an instantaneous global traffic cutover.

Production patch

Use a shared database-backed idempotency boundary, versioned configuration, and explicit worker ownership. For distributed leadership, use a lease mechanism rather than relying on the presence of only one replica.

CREATE TABLE IF NOT EXISTS service_leases (
    lease_name TEXT PRIMARY KEY,
    holder_id TEXT NOT NULL,
    expires_at TIMESTAMPTZ NOT NULL,
    fencing_token BIGINT NOT NULL
);

Lease acquisition and renewal must be implemented transactionally. The database clock should determine expiration, and the fencing token must increase whenever ownership changes.

BEGIN;

INSERT INTO service_leases (
    lease_name,
    holder_id,
    expires_at,
    fencing_token
)
VALUES (
    'checkout-worker',
    $1,
    NOW() + INTERVAL '15 seconds',
    1
)
ON CONFLICT (lease_name)
DO UPDATE SET
    holder_id = EXCLUDED.holder_id,
    expires_at = NOW() + INTERVAL '15 seconds',
    fencing_token = service_leases.fencing_token + 1
WHERE service_leases.expires_at <= NOW()
RETURNING fencing_token;

COMMIT;

Patch analysis:

  • Only a worker that successfully acquires or renews the lease should process exclusive work.
  • Fencing tokens must be checked by downstream state-changing operations. A lease by itself cannot stop a stale worker that continues after losing ownership.
  • Use shared idempotency records to protect external side effects such as payment processing, because a distributed lease does not guarantee exactly-once delivery.

Deployment correction: Keep configuration schemas backward-compatible and coordinate background worker activation separately from HTTP traffic promotion. Verify old connections, queues, and scheduled jobs before terminating the previous environment.

11. Security Audit, Access Control and Backpressure

A deployment system has access to container registries, application configuration, production clusters, and sometimes database credentials. An incorrectly configured CI/CD identity can be more dangerous than an application vulnerability.

Container isolation

Run application containers as non-root users. Remove unnecessary Linux capabilities, enable a seccomp profile, prevent privilege escalation, and use a read-only root filesystem wherever the application supports it.

Avoid privileged containers for ordinary API services. Use restricted Pod Security admission policies and separate workloads with different trust levels into appropriately controlled namespaces and nodes.

Least-privilege service accounts

Applications that do not call the Kubernetes API should not receive an automatically mounted API token. If API access is required, create a dedicated ServiceAccount and grant only the specific permissions needed.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: checkout-api
  namespace: production
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: checkout-config-reader
  namespace: production
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    resourceNames: ["checkout-config"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: checkout-config-reader
  namespace: production
subjects:
  - kind: ServiceAccount
    name: checkout-api
    namespace: production
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: checkout-config-reader

This example grants read access to one named ConfigMap. The Deployment must explicitly reference the ServiceAccount if the workload requires it. If the application does not need Kubernetes API access, omit that identity and keep token automounting disabled.

Zero-trust identity and token rotation

Avoid long-lived credentials embedded in container images, environment files, Git repositories, or deployment manifests. Prefer short-lived workload identity and cloud-provider federation where supported.

Kubernetes projected ServiceAccount tokens can provide bounded token lifetimes and audience-specific identity. External systems should validate token issuer, audience, signature, and expiry rather than trusting a token simply because it originated from a Pod.

Rotate database passwords, signing keys, and service credentials using a controlled process. During rotation, allow an overlap period when both old and new credentials are valid, update consumers, verify successful authentication, and revoke the previous credential.

Mutual TLS between services

Mutual TLS authenticates both sides of a connection. It is particularly useful for sensitive service-to-service communication where network location alone should not establish trust.

A service mesh or a well-managed internal PKI can automate certificate issuance, identity validation, and certificate rotation. Avoid hardcoding certificates inside images. Ensure that certificate expiry and rotation failures are observable.

Backpressure during deployment

Backpressure prevents an overloaded application from accepting more work than it can safely process. Without it, a new version may receive traffic faster than it can initialize its caches, acquire database connections, or drain its internal queues.

Token Bucket algorithm

A token bucket maintains a limited number of tokens. Each incoming request consumes a token. Tokens are replenished at a configured rate up to the maximum bucket capacity.

This allows controlled bursts while maintaining a long-term average request rate. It is useful for API gateways and service-level admission control.

Leaky Bucket algorithm

A leaky bucket processes requests at a controlled output rate. Excess requests wait in a bounded queue or are rejected when the queue is full.

This produces smoother processing rates but can increase waiting time. A bounded queue is essential; an unlimited queue simply turns overload into memory exhaustion and latency growth.

Both algorithms should use shared state or gateway-level coordination if a global limit is required across multiple application replicas. An in-memory limiter on each Pod enforces a per-Pod limit, not a cluster-wide limit.

During canary deployment, adaptive rate limits can protect a smaller candidate pool. Monitor queue depth, rejected requests, CPU saturation, and response latency. If the canary cannot keep up, reduce its traffic weight rather than allowing requests to accumulate indefinitely.

12. Production Best Practices and Security Checklist

Production Deployment Checklist

Application lifecycle

  • Implement separate startup, readiness, and liveness checks.
  • Handle SIGTERM and stop accepting new work before process termination.
  • Drain HTTP connections and finish active requests within the termination grace period.
  • Make queue consumers safe against duplicate delivery and interrupted processing.
  • Keep old and new application versions compatible during rollout.

Kubernetes resource management

  • Set realistic CPU and memory requests based on observed production usage.
  • Set memory limits with room for runtime overhead and native allocations.
  • Calculate maxSurge against actual schedulable cluster capacity.
  • Use topology spread constraints or anti-affinity where failure-domain separation is required.
  • Configure PodDisruptionBudgets for supported voluntary eviction scenarios.

Container optimization

  • Build immutable images and deploy by digest when supply-chain traceability is required.
  • Use multi-stage Docker builds and remove unnecessary packages.
  • Run as a non-root user.
  • Use vulnerability scanning and signed image verification.
  • Do not include credentials, private keys, or development tools in production images.

Networking and caching

  • Configure finite connection timeouts and bounded retry policies.
  • Drain connections gracefully during termination.
  • Use appropriate cache TTLs and bounded cache capacity.
  • Make cache invalidation and version compatibility explicit.
  • Validate gateway health checks and routing behavior during traffic switches.

Release maintenance

  • Record immutable image versions and deployment revisions.
  • Maintain a tested rollback procedure.
  • Use backward-compatible database migrations.
  • Monitor release-specific error and latency metrics.
  • Review deployment events and failed rollout history.
  • Run periodic disaster-recovery and rollback exercises.

13. Advanced Architectural Trade-Offs

13.1 Should maxUnavailable always be zero?

No. Setting maxUnavailable to zero ensures that the Deployment controller does not intentionally reduce the available replica count below the desired count during a normal rolling update. However, it requires enough spare capacity to create replacement Pods before removing old ones.

A small stateless API with sufficient cluster headroom can use maxUnavailable zero and maxSurge one. A large workload with tight capacity constraints may need a balanced combination.

The decision should be based on redundancy, failure tolerance, startup duration, and schedulable resources, not on the assumption that zero unavailable Pods is always the safest setting.

13.2 Rolling deployment or blue-green for database-heavy applications?

Rolling updates are efficient when application versions remain compatible with the same database schema. They become risky when a release introduces incompatible queries, changes column semantics, or removes fields still used by old application replicas.

Blue-green provides a cleaner environment boundary, but both environments may still share the same database. If green applies a destructive migration, blue can become unusable immediately after the switch.

Use expand-and-contract migrations: add new schema elements first, deploy code that supports both old and new representations, migrate data, and remove obsolete schema only after the rollback window has closed.

13.3 Is canary always safer than rolling updates?

Not automatically. Canary reduces exposure to a defective release, but it adds routing and analysis complexity. If a canary receives too little traffic, important defects may remain undetected. If it receives too much traffic, a broken release can still affect many users.

A canary also needs independent capacity. Sending 5% of traffic to one undersized Pod may create an artificial performance failure that does not reflect the behavior of a properly scaled release.

13.4 How much extra memory does blue-green require?

There is no universal memory overhead percentage. It depends on the number of replicas, runtime, cache size, native allocations, and workload behavior.

For the example configuration with six replicas requesting 512 MiB each, blue and green together request approximately 6 GiB of application memory. That is double the six-replica baseline request of 3 GiB, before considering system overhead and other workloads.

Measure actual working-set memory during peak traffic and startup. Requests and limits are scheduling and enforcement settings, not guaranteed predictions of actual memory consumption.

13.5 How should long-running requests be handled?

WebSockets, streaming responses, large file transfers, and long-running transactions need explicit draining behavior. A Pod becoming unready does not guarantee that every existing connection will immediately disappear.

Implement application-level shutdown hooks, finite request deadlines, and client reconnection behavior. For WebSockets, provide a controlled close signal and reconnect strategy. For workers, stop fetching new jobs before acknowledging termination.

13.6 Does a PodDisruptionBudget prevent deployment downtime?

A PDB limits certain voluntary disruptions, such as eviction during node maintenance. It is not a universal availability guarantee and does not replace rolling-update strategy parameters.

Node crashes, infrastructure failures, application deadlocks, and other involuntary failures can still reduce availability. The workload must tolerate those events through sufficient replicas and failure-domain distribution.

13.7 When should progressive delivery be automated?

Automation becomes worthwhile when releases are frequent, services have meaningful traffic, and the team has trustworthy metrics and well-defined rollback thresholds.

Tools such as Argo Rollouts or Flagger can coordinate staged deployments and metric analysis. They do not eliminate the need to design health checks, compatibility boundaries, and rollback-safe database changes.

13.8 How do you choose a rollback threshold?

Avoid relying on a single universal error-rate threshold. A payment API and a background reporting service have different failure costs and traffic patterns.

Define thresholds relative to the stable baseline and business SLOs. For example, a team might abort when the canary's 5xx rate exceeds a defined absolute limit or materially exceeds the stable version for a sustained observation window, provided the sample count is statistically meaningful.

Account for low traffic, regional variation, retry amplification, and delayed failures. A deployment system should be able to pause promotion when evidence is insufficient rather than interpreting missing metrics as success.

14. Frequently Asked Questions

1. Does Kubernetes automatically provide zero-downtime deployments?

Kubernetes supports rolling updates for Deployments, but uninterrupted service depends on correct readiness checks, sufficient resources, application compatibility, and graceful shutdown. A successful Deployment rollout alone is not a guarantee of zero customer-facing errors.

2. What is the difference between maxSurge and maxUnavailable?

maxSurge controls how many additional Pods can be created above the desired replica count. maxUnavailable controls how many desired replicas may be unavailable during the rollout. Together they determine the rollout's capacity and replacement behavior.

3. What happens when a new Kubernetes Pod fails its readiness probe?

Kubernetes marks the Pod as not ready and excludes it from the ready endpoints of matching Services. The Deployment controller cannot count that Pod as available, so the rollout may stop progressing if sufficient replacements never become ready.

4. How do I roll back a Kubernetes deployment?

Run kubectl rollout undo deployment/checkout-api -n production to restore the previous recorded Pod template. Then monitor rollout status, inspect Pods, and validate application metrics. This does not reverse database or external-system changes.

5. Which deployment strategy is best for production?

Rolling updates are a practical default for most stateless services. Blue-green is useful when a complete candidate environment must be validated before switching traffic. Canary is appropriate when gradual exposure and production traffic analysis are important.

6. Can I implement a canary deployment using only a Kubernetes Service?

A standard Service does not provide reliable percentage-based traffic splitting between versions. Use a compatible Gateway API implementation, service mesh, ingress controller with supported weighting, or progressive-delivery controller.

7. Does a blue-green deployment require twice the infrastructure?

It can require approximately twice the application capacity during full coexistence if both environments use the same replica count and resource requests. Actual infrastructure overhead depends on how much of the environment is duplicated and whether shared services are reused.

15. Final Engineering Recommendation

Zero-downtime deployment is not a Kubernetes feature that can be enabled with one YAML property. It is the result of coordinating workload lifecycle, traffic routing, resource capacity, application compatibility, and operational recovery.

For most production APIs, begin with rolling updates using a carefully selected maxSurge and maxUnavailable configuration. Add startup and readiness probes, bounded resources, graceful shutdown, and a tested rollback procedure.

Choose blue-green when independent release validation and fast environment switching justify the additional infrastructure. Choose canary when you need controlled exposure to real production traffic and have the monitoring needed to make promotion decisions.

The most valuable deployment improvement is often not a more sophisticated release strategy. It is fixing the application assumptions that make old and new versions unsafe to run together.

Key Takeaway

Keep enough healthy capacity online, make application versions backward-compatible, measure the behavior of the new release, and ensure rollback can happen without corrupting shared state. That is the foundation of a production-grade zero-downtime deployment system.

Technical examples are illustrative reference configurations. Validate Kubernetes API versions, Gateway API controller support, resource requirements, network behavior, and application-specific shutdown semantics in a staging environment before production use.

Comments