Rolling, blue-green and canary deployments, the health checks and graceful shutdown that make them work, and the expand and contract pattern for database migrations that keeps old and new code running side by side.
A deploy that drops requests for a few seconds is easy to ignore on a side project and expensive on a payments API, where those seconds become failed checkouts, retried webhooks and support tickets. Zero-downtime deployment is a set of practices that together keep every request served while a new version replaces the old one, and most of the failures come from the parts outside the deploy strategy itself: health checks that pass too early, processes that exit with requests in flight, and migrations that break the version still running.
Rolling deployment replaces instances in batches: the orchestrator starts new instances, waits for them to become healthy, shifts traffic to them, and stops old ones, repeating until the release is done. It needs little extra capacity and is the default on ECS, Kubernetes and Cloud Run. During the rollout, old and new versions serve traffic at the same time, which is the property every other practice in this guide has to account for.
Blue-green deployment runs the new version as a full second environment and moves all traffic at once, usually at the load balancer. Rollback means switching back, at the cost of double capacity during the release. The database is still shared, so migrations need the same care as with rolling releases.
Canary deployment sends a small share of traffic, such as 5 percent, to the new version and widens it as error rates and latency stay healthy. It catches regressions that only show under production traffic, at the cost of traffic-splitting infrastructure and metrics good enough to decide on.
An instance should only receive traffic once it can serve it. A readiness check that returns 200 as soon as the HTTP server binds will route requests to an instance that has not connected to its database yet. A useful readiness check confirms the process has finished startup work, such as connecting pools and loading config, without calling every downstream dependency on each probe, which would turn a slow dependency into a fleet-wide outage.
Liveness checks are separate and should only fail when the process is stuck, since a failing liveness check restarts the instance.
When an old instance is stopped, the platform sends SIGTERM, removes it from the load balancer and, after a timeout, sends SIGKILL. The process has to stop accepting new connections, finish in-flight requests, and close pools within that window. In Go, that is signal.NotifyContext for SIGTERM followed by http.Server.Shutdown(ctx) with a deadline shorter than the platform timeout. Long-lived connections such as WebSockets and server-sent events need clients that reconnect, since a deploy will close them.
A background worker processing a queue message when it is stopped should either finish within the window or let the message return to the queue, which only works if handlers are idempotent and can safely run twice.
During any rolling or canary release, the old code keeps running against the new schema. A migration that renames a column, drops one, or adds a NOT NULL column without a default breaks the old version the moment it runs.
The expand and contract pattern splits breaking changes into compatible steps:
On large Postgres tables, locks matter as well: CREATE INDEX CONCURRENTLY avoids blocking writes during index builds, and a lock_timeout on migration sessions stops a migration from queueing behind a long transaction and blocking everything behind it.
Encore is infrastructure from code for TypeScript and Go, with a platform that provisions and deploys into your own AWS or GCP account. On AWS, services run on Fargate, and Encore orchestrates a rolling update that introduces new tasks gradually while removing old ones, as described in the AWS infrastructure docs. On GCP, Cloud Run services are configured for zero-downtime deployment with health check grace periods so containers have time to start before they receive traffic, per the GCP infrastructure docs.

Migrations run in sequence on each deploy, and Encore applies them before the application restarts with the new code, so the docs on changing the database schema ask that the old code keeps working with the new schema. Expand and contract fits that rule, and splitting a name column into display_name starts with an additive migration:
-- users/migrations/2_add_display_name.up.sql
ALTER TABLE users ADD COLUMN display_name TEXT;
UPDATE users SET display_name = name WHERE display_name IS NULL;
The service writes both columns until every running instance has the new code:
// users/users.go
package users
import (
"context"
"encore.dev/storage/sqldb"
)
var db = sqldb.NewDatabase("users", sqldb.DatabaseConfig{
Migrations: "./migrations",
})
type RenameParams struct {
DisplayName string
}
//encore:api public method=PUT path=/users/:id/name
func Rename(ctx context.Context, id int64, p *RenameParams) error {
_, err := db.Exec(ctx, `
UPDATE users SET name = $1, display_name = $1 WHERE id = $2
`, p.DisplayName, id)
return err
}
The migration in users/migrations/1_create_users.up.sql is the starting schema that the expand migration above builds on:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL
);
A later release switches reads to display_name, and a third migration drops name once no deployed code reads it. For a large table, run the backfill in batches from a cron job or a one-off endpoint rather than in the migration itself.
Run in your terminal to get started locally.
encore run applies the migrations to a local Postgres, so you can run the old and new code against the expanded schema before you push. Deploys go out on each push through the GitHub integration, each with a build and test phase before the rollout begins.
It is a way of releasing a new version of a service so that requests keep succeeding throughout the release. New instances start and pass health checks before they receive traffic, and old instances finish in-flight requests before they stop.
A rolling deployment replaces instances a few at a time within one environment. Blue-green runs a complete second copy of the new version and switches traffic over at once, which makes rollback a switch back but needs double the capacity during the release.
Make every migration compatible with both the old and the new code. Add columns as nullable or with defaults, backfill, deploy code that uses them, and only drop or tighten the old structure in a later release, a sequence called expand and contract.
Yes, Encore runs services on AWS Fargate with a rolling update that introduces new tasks while removing old ones, and on GCP it configures Cloud Run services with zero-downtime deployment and health check grace periods. Database migrations run before the new code starts, so they need to work with the old code too.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.