2026-03-16 · 8 min read · Serhii Mazurok
Your users should never know you deployed. Here's how to make that happen on Kubernetes.
A basic Kubernetes Deployment with RollingUpdate strategy will update pods one at a time. But without proper configuration, you'll still drop requests during rollouts. Here's why:
1. Pod receives SIGTERM but is still in the Service endpoint list 2. Readiness probe hasn't been configured or is too slow 3. Application doesn't handle graceful shutdown - connections are cut mid-request 4. Load balancer hasn't removed the pod from its pool yet
Let's fix each of these.
The readiness probe tells Kubernetes when a pod is ready to receive traffic. Without it, traffic is sent to pods that haven't finished starting.
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
successThreshold: 1
Rules:
/healthz should return 200 only when the app is truly ready (DB connected, caches warmed)initialDelaySeconds should cover your app's startup timeperiodSecondsWhen Kubernetes sends SIGTERM, your app should:
1. Stop accepting new connections 2. Finish processing in-flight requests 3. Close database connections cleanly 4. Exit
// Go example
srv := &http.Server{Addr: ":8080"}
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM)
<-sigCh
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
srv.Shutdown(ctx)
}()
Set terminationGracePeriodSeconds to match:
spec:
terminationGracePeriodSeconds: 30
There's a race condition: Kubernetes sends SIGTERM and removes the pod from endpoints simultaneously. The kube-proxy / ingress controller may not have updated yet, so traffic can still arrive at a terminating pod.
The fix: add a short delay before your app starts shutting down.
lifecycle:
preStop:
exec:
command: ["sleep", "5"]
This gives the network 5 seconds to drain the pod from all load balancer pools before the app starts shutting down.
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # never reduce below desired count
maxSurge: 1 # add 1 extra pod during rollout
maxUnavailable: 0 is critical - it ensures the old pod stays running until the new pod passes its readiness probe.
Protect against voluntary disruptions (node drains, cluster upgrades):
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: app-pdb
spec:
minAvailable: 1 # at least 1 pod must be running at all times
selector:
matchLabels:
app: my-app
For larger deployments, use maxUnavailable: 25% instead of minAvailable to allow faster rollouts while maintaining capacity.
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
spec:
terminationGracePeriodSeconds: 30
containers:
- name: app
image: app:v1.2.3
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
lifecycle:
preStop:
exec:
command: ["sleep", "5"]
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: app-pdb
spec:
minAvailable: 1
selector:
matchLabels:
app: app
| Check | Why |
|---|---|
| Readiness probe configured | Traffic only goes to ready pods |
maxUnavailable: 0 | Old pod stays up until new one is ready |
preStop: sleep 5 | Drains pod from LB before shutdown |
| Graceful shutdown handles SIGTERM | In-flight requests complete |
terminationGracePeriodSeconds ≥ shutdown time | K8s waits long enough |
| PodDisruptionBudget set | Node drains don't kill all pods |