Once there are two or more instances behind a load balancer, the maintenance window stops being necessary. You release to one backend at a time, and the pool serves traffic throughout. The mechanics take an afternoon to get right and then you stop deploying at midnight.
For each backend, in turn: stop sending it new requests, let the requests it already has finish, deploy, wait for its health check to pass, put it back. Move to the next one only after the previous instance is healthy again.
The important property is that the pool never loses more capacity than one instance at a time — which means the remaining instances must be able to carry peak traffic without it. A load-balanced pair running at 60% each is not actually redundant; it is two servers that will both fall over politely.
Removing a backend abruptly cuts every request it was serving. Draining means the load balancer stops choosing it for new connections while existing ones complete.
The clean way to do that from inside the application is the readiness endpoint: on shutdown signal, start returning 503 from /healthz, keep serving in-flight requests normally, and only then exit. The load balancer notices within an interval or two and stops routing to it. This is why the failure threshold matters in both directions — a threshold of three at a ten-second interval means up to thirty seconds of new requests still arriving after you begin the drain.
Then let the process finish what it started. Your web server's graceful shutdown period should be longer than your slowest normal request, and your deploy script should wait for the process to exit rather than killing it. Requests dropped during a deploy are almost always a shutdown that was faster than the traffic.
Two settings quietly undo good draining.
Keep-alive connections outlive individual requests, so a client can keep reusing a connection to an instance you are trying to retire. Keep the backend's keep-alive timeout modest — a few seconds — so connections recycle quickly during a rollout.
Sticky sessions pin a client to one backend, so draining that backend means those users are moved to a machine that does not have their session unless the session store is shared. If you are running rolling deploys with stickiness on, move sessions into a shared store first; otherwise every release logs a slice of your users out.
During a rolling deploy, two versions of the application are live at once. The database schema has to be compatible with both, which means splitting the change:
Renames and NOT NULL constraints added in one step are the two that bite. If a change genuinely cannot be made backwards-compatible, that is the case for a short blue-green cutover rather than a rolling release.
Blue-green runs two complete fleets and shifts traffic between them: the release is a single change of which pool the load balancer targets, and the rollback is the same change in reverse. It costs double capacity for the duration and it needs the same backwards-compatible data work if both fleets talk to one database, but it turns a rollback into seconds.
Rolling is the right default for routine releases. Blue-green earns its cost for risky ones — a framework upgrade, a rewritten checkout, anything where "undo" needs to be immediate.
Run a modest constant load against the public address and perform a full rollout. If the error count is zero and latency stays inside its normal band, the deploy is genuinely zero-downtime. If it is not, you have learned which of draining, keep-alive or shutdown timing is wrong — before a customer does.
Antyxsoft Load Balancers give you active health checks, tag-targeted backend pools and round robin or least connections distribution, so rolling and blue-green releases need no maintenance window — see how Load Balancers work.