Zero-Downtime Rolling Updates for Django on Kubernetes
Make Kubernetes rolling updates of Django truly zero-downtime: configure maxSurge and maxUnavailable, readiness probes that reflect real readiness, preStop delays for endpoint propagation, Gunicorn graceful shutdown, backward-compatible migrations, and minReadySeconds.
Problem statement
Every deploy of your Django app on Kubernetes produces a handful of 502 or 503 errors, or a few seconds of connection resets. Rolling updates are supposed to be seamless, but several timing details must line up: new pods must not receive traffic before they are ready, old pods must stop receiving traffic before they stop serving, in-flight requests must finish, and the database schema must work for both versions during the rollout.
- Set
strategy.rollingUpdate.maxUnavailable: 0andmaxSurge: 1(or 25%) so capacity never drops. - Use a readiness probe that passes only when Django can serve requests, and
minReadySecondsto catch crash loops. - Add a
preStopsleep (5 to 15 seconds) so endpoints and load balancers stop routing before SIGTERM. - Let Gunicorn finish in-flight requests on SIGTERM, with
terminationGracePeriodSecondslonger than the preStop plusgraceful_timeout. - Run backward-compatible migrations before the rollout, so old and new pods both work with the schema.
Step-by-step solution
1. Configure the strategy
spec:
replicas: 3
minReadySeconds: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
With maxUnavailable: 0, Kubernetes starts a new pod and waits until it is Ready (and stays Ready for minReadySeconds) before terminating an old one. Capacity stays at or above the replica count throughout.
2. Make readiness meaningful
readinessProbe:
httpGet:
path: /healthz/ready
port: 8000
httpHeaders: [{ name: Host, value: shop.example.com }]
periodSeconds: 5
failureThreshold: 2
startupProbe:
httpGet: { path: /healthz/live, port: 8000, httpHeaders: [{ name: Host, value: shop.example.com }] }
periodSeconds: 5
failureThreshold: 30
The readiness endpoint should check that Django loaded and the database is reachable. Gunicorn with --preload or a warm-up step avoids the first real request paying import costs. A startup probe gives slow boots time without making liveness too lenient.
3. Delay shutdown until traffic stops
When a pod is terminated, Kubernetes removes it from Service endpoints and sends SIGTERM at roughly the same time. Load balancers, ingress controllers, and kube-proxy on every node take a few seconds to notice the endpoint change, so requests keep arriving at a pod that is already shutting down. A preStop hook delays SIGTERM:
lifecycle:
preStop:
exec:
command: ["sleep", "10"]
terminationGracePeriodSeconds: 45
4. Shut down Gunicorn gracefully
On SIGTERM, Gunicorn stops accepting new connections and lets workers finish current requests for up to graceful_timeout (default 30 seconds). Keep terminationGracePeriodSeconds larger than preStop plus graceful_timeout, or the kubelet sends SIGKILL mid-request. Make sure Gunicorn is PID 1 or receives the signal: use exec-form CMD or exec gunicorn in entrypoint scripts.
5. Keep migrations compatible
During the rollout, old and new pods serve traffic at the same time against one schema. Run migrations in a Job before updating the Deployment, and only use migrations the old code tolerates: add nullable columns or new tables, never rename or drop in the same release. See Zero-Downtime Django Deployment.
Explanation
A rolling update replaces pods gradually, but Kubernetes components converge asynchronously: endpoint changes reach kube-proxy and ingress controllers with a delay, and load balancer target groups take longer still. Without a delay before shutdown, a terminating pod receives requests it can no longer serve. Readiness probes and minReadySeconds protect the other end, keeping traffic away from pods that are not ready. Backward-compatible migrations handle the remaining gap: two versions of code running against one database.
Edge cases / notes
- Long-lived connections. WebSockets and streaming responses do not finish within a grace period; clients must reconnect, and the app should handle it.
- Cloud load balancers. With load balancers targeting pod IPs directly, deregistration delays may need to match the preStop duration.
- Celery workers. Workers need longer grace periods and
acks_latewith idempotent tasks rather than preStop delays.
Next steps
- Plan the full release process in Zero-Downtime Django Deployment.
- Protect against node drains with Kubernetes PodDisruptionBudgets for Django.
- Handle signals with Django Graceful Shutdown on SIGTERM.
FAQ
Why do I see 502 errors during Kubernetes deploys?
Usually traffic still reaches pods after SIGTERM. Add a preStop sleep and make sure Gunicorn shuts down gracefully.
What should maxUnavailable be?
0 for web Deployments, so a new pod is ready before an old one stops.
How long should the preStop sleep be?
Long enough for endpoint and load balancer updates, typically 5 to 15 seconds.
Can migrations run during the rollout?
Run them before the rollout, and only use changes that the old code also works with.