Zero-Downtime Deploys Without The Mythology
"Zero-downtime" sounds like a magic spell. In practice it's three boring disciplines applied consistently: backward-compatible changes, graceful shutdown, and health checks that tell the truth.
Backward-compatible first
The single biggest source of deploy outages isn't the deploy mechanism — it's a new version that can't coexist with the old one for the 30 seconds they overlap.
-- Don't do this in one deploy:
ALTER TABLE users DROP COLUMN legacy_token;
-- Do this across two:
-- Deploy 1: stop writing to legacy_token
-- Deploy 2: drop the columnEvery schema change is two deploys. Every API change is additive until the old clients are gone.
Graceful shutdown
When the orchestrator sends SIGTERM, your process has a few seconds to stop
accepting new connections and drain in-flight ones:
srv := &http.Server{Addr: ":8080"}
go srv.ListenAndServe()
<-ctx.Done() // SIGTERM received
shutdownCtx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
srv.Shutdown(shutdownCtx) // finishes live requests, refuses new onesHealth checks that don't lie
A readiness probe that returns 200 the instant the process starts is worse
than no probe — it tells the load balancer to send traffic before the app can
serve it. Gate readiness on the things that actually matter:
- Database connection pool established
- Caches warmed (if you depend on them)
- Downstream dependencies reachable
Get those three right and "zero-downtime" stops being mythology and starts being Tuesday.