Every deployment is a risk. The question is how much risk you’re willing to accept — and whether your users ever need to notice.
Zero-downtime deployments on Kubernetes are achievable with the right configuration. They’re also easy to get wrong in ways that only surface under production load. This guide covers what actually works, based on running migrations for e-commerce platforms, SaaS products and fintech applications where downtime has a direct revenue cost.
Why deployments cause downtime in the first place
Most Kubernetes deployments cause downtime for one of three reasons.
The new pod starts receiving traffic before it’s ready. Kubernetes marks a pod as running the moment the container starts — but your application might need 10, 20 or 30 seconds to initialise before it can handle requests. Without a readiness probe, Kubernetes sends traffic to a pod that isn’t ready yet.
The old pod is terminated before the new one is fully serving traffic. If your rolling update replaces pods faster than new ones become ready, there’s a window where neither the old nor the new pod is handling requests cleanly.
The application doesn’t handle SIGTERM gracefully. When Kubernetes terminates a pod, it sends SIGTERM and waits for the process to exit. If your application exits immediately without finishing in-flight requests, those requests fail.
The foundation: readiness and liveness probes
Readiness probes are the single most important configuration for zero-downtime deployments. Without them, nothing else matters.
yaml
readinessProbe:
httpGet:
path: /health/ready
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3
successThreshold: 1
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
The readiness probe tells Kubernetes when the pod is ready to receive traffic. The liveness probe tells Kubernetes when the pod has entered a broken state and should be restarted.
These are different things. A pod can be live (the process is running) but not ready (still loading configuration, warming up caches, waiting for a database connection). Your application should expose separate endpoints for each.
A minimal health endpoint in Node.js:
javascript
// Readiness — app is ready to handle requests
app.get('/health/ready', (req, res) => {
if (db.isConnected() && cache.isReady()) {
res.status(200).json({ status: 'ready' });
} else {
res.status(503).json({ status: 'not ready' });
}
});
// Liveness — app is alive (not deadlocked)
app.get('/health/live', (req, res) => {
res.status(200).json({ status: 'alive' });
});
Rolling update strategy
Kubernetes uses a RollingUpdate strategy by default, but the default configuration is too aggressive for production workloads:
yaml
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
maxUnavailable: 0 means Kubernetes will never terminate an old pod until a new pod is fully ready. This is the key setting. With the default maxUnavailable: 1, Kubernetes can terminate an old pod immediately, leaving you briefly under capacity.
maxSurge: 1 allows one extra pod during the rollout. If you run 3 replicas, during deployment you’ll temporarily have 4. Make sure your cluster has capacity for this.
Graceful shutdown
Your application must handle SIGTERM and finish in-flight requests before exiting:
javascript
process.on('SIGTERM', () => {
console.log('SIGTERM received — shutting down gracefully');
server.close(() => {
console.log('All connections closed');
process.exit(0);
});
// Force exit after 30 seconds if connections haven't closed
setTimeout(() => {
console.error('Forcing exit after timeout');
process.exit(1);
}, 30000);
});
Set terminationGracePeriodSeconds in your pod spec to give the application enough time to drain connections:
yaml
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
...
The sequence when Kubernetes terminates a pod: SIGTERM is sent → pod is removed from the service endpoints (no new traffic) → application finishes in-flight requests → process exits. The key detail: pod removal from endpoints and SIGTERM happen simultaneously. There’s a propagation delay of a few seconds before all nodes stop routing traffic to the pod. Add a preStop sleep to account for this:
yaml
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
This 5-second sleep gives the endpoint removal time to propagate before your application starts refusing connections.
PodDisruptionBudgets
Rolling updates aren’t the only thing that terminates pods. Node maintenance, cluster upgrades and autoscaling can all evict pods simultaneously. A PodDisruptionBudget sets a floor on availability:
yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: app-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: your-app
With minAvailable: 2 and 3 replicas, Kubernetes will never evict more than 1 pod at a time — even during node maintenance. Without this, a node drain can terminate all pods on that node simultaneously.
Deployment verification with kubectl rollout
After deploying, watch the rollout in real time:
bash
kubectl rollout status deployment/your-app --timeout=5m
This command blocks until the rollout completes or times out. Use it in your CI/CD pipeline to catch failed deployments before they affect users:
yaml
# In your CI/CD pipeline
- kubectl apply -f k8s/deployment.yaml
- kubectl rollout status deployment/your-app --timeout=5m || kubectl rollout undo deployment/your-app
The || kubectl rollout undo automatically rolls back if the deployment doesn’t complete within 5 minutes — for example, if new pods fail their readiness probe.
A real-world example
An e-commerce client came to me before Black Friday with a deployment process that caused 5 minutes of downtime every release. Their development team had stopped deploying on Fridays because they were afraid of breaking the store before the weekend.
The issues: no readiness probes (traffic hit pods before the app was ready), maxUnavailable: 1 (pods were terminated before replacements were healthy), no graceful shutdown (in-flight checkout requests were dropped on pod termination).
After implementing the configuration above, their next deployment was invisible to users. Deploy time dropped from “avoid it on Fridays” to a 90-second rollout that runs in CI/CD on every merge to main. That Black Friday — their highest traffic day — was the first one without an incident.
Summary configuration
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: your-app
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
image: your-app:latest
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
readinessProbe:
httpGet:
path: /health/ready
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
Zero-downtime deployments aren’t a Kubernetes feature you enable — they’re a combination of correct probe configuration, a conservative rolling update strategy and an application that handles shutdown gracefully. Each piece matters. Miss one and you’ll see failures under load.
If your team is still afraid of deployments, or if you’re planning a migration to Kubernetes and want to get this right from the start — that’s exactly the kind of work I do. Fixed scope, defined timeline, measurable outcome.
Get in touch → devops24.eu/#contact
