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.
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.
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 migration | Gradual Pod replacement | Switch between environments | Progressive traffic weighting |
| Infrastructure cost | Low to moderate | High during coexistence | Moderate |
| Traffic control | Limited | Environment-level | Fine-grained |
| Rollback | Deployment revision | Route back to old environment | Reduce canary weight to zero |
| Release complexity | Low | Moderate | High |
| Best application | Most stateless APIs | High-risk releases | Traffic-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.
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:
| Configuration | maxSurge | maxUnavailable | Behavior |
|---|---|---|---|
| Balanced | 2 | 1 | Up to 8 Pods, one unavailable |
| Capacity-first | 2 | 0 | Preserves desired availability |
| Cost-first | 0 | 1 | Replaces without surge capacity |
| Conservative | 1 | 0 | One 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.
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 1Create 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:
maxSurge: 2permits two additional Pods, whilemaxUnavailable: 1allows one unavailable replica during the rollout. This balances deployment speed and capacity.- 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.
- 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 2Create 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:
- The Service selects Pods using stable application labels rather than individual Pod names or IP addresses.
- 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 3Create 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:
- The budget requires five healthy Pods out of the six desired replicas for supported voluntary evictions.
- 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.
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:
kubectl applycreates or updates the declarative resources. Applying the namespace first avoids a namespace-not-found error.kubectl rollout statuswaits 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 5kubectl 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:
- Changing the image updates the Deployment Pod template and triggers a new revision and ReplicaSet.
- 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:
- The
track: bluelabel identifies the stable environment. The Deployment selector must match the Pod template labels. - 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:
- Green starts six independent replicas. This doubles the application Pod count during coexistence, excluding infrastructure and monitoring overhead.
- 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:
- The production Service initially routes traffic only to blue Pods that are ready and match both labels.
- 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:
- 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.
- 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:
- Rollback changes the active Service selector. It does not rebuild or redeploy blue.
- 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.
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:
- Stable and canary have different selectors, so they can scale independently and receive separate Service traffic.
- 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:
- Each Service exposes only its matching release. This provides independent backends for the gateway.
- 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:
- 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.
- 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:
| Stage | Stable | Canary | Decision |
|---|---|---|---|
| Initial | 95% | 5% | Check basic health |
| Stage 2 | 80% | 20% | Compare latency and errors |
| Stage 3 | 50% | 50% | Evaluate sustained traffic |
| Final | 0% | 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:
- The route is changed to send traffic entirely to stable. Confirm that the gateway has accepted the new configuration before terminating canary capacity.
- Scaling canary to zero is a cleanup action, not the traffic rollback itself. Keep the stable Deployment untouched during the recovery.
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:
- Rollout history displays recorded Deployment revisions. Revisions are created when the Pod template changes, not for every ordinary scaling change.
- 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:
rollout undorestores the previous recorded Pod template. It does not reverse database migrations, external configuration changes, or third-party side effects.- 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
finallyblock 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.

Comments