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.

Seamless rolloutNew pod startsreadiness gateTraffic shiftsold pod removedpreStop delayendpoints updateGraceful exitrequests finishZero downtime needs readiness, endpoint propagation, and graceful shutdown to line up.
  1. Set strategy.rollingUpdate.maxUnavailable: 0 and maxSurge: 1 (or 25%) so capacity never drops.
  2. Use a readiness probe that passes only when Django can serve requests, and minReadySeconds to catch crash loops.
  3. Add a preStop sleep (5 to 15 seconds) so endpoints and load balancers stop routing before SIGTERM.
  4. Let Gunicorn finish in-flight requests on SIGTERM, with terminationGracePeriodSeconds longer than the preStop plus graceful_timeout.
  5. 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.

setting and purposePreventsTypical valuemaxUnavailable: 0capacity drop0maxSurge: 1slow rollouts1 or 25%readinessProbetraffic to cold podschecks DBpreStop sleeprequests to exiting pods10 sterminationGracePeriodkilled mid-requestpreStop + 30 sEach setting closes one gap in the handover between old and new podsdjango-deployment.com
Most leftover errors come from the missing preStop delay.

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.

new pod in, old pod outNew pod ReadyminReadySecondsOld pod terminatingendpoint removedpreStop sleep 10 sstill servingSIGTERM to Gunicornfinish requestsOld pod exitsbefore grace periodThe preStop window covers the time routing takes to catch updjango-deployment.com
Repeat for each replica; capacity never drops below the replica count.

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_late with idempotent tasks rather than preStop delays.

Next steps

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.

Learning path · step 9 of 16

Django on Kubernetes

Keep going