BackendSeptember 15, 2026 · 9 min

Zero-downtime database migrations: shipping schema changes without a maintenance window

The scariest deploy is the one that touches the database. Rename a column in one migration and the old code — still running for a few seconds during rollout — breaks instantly, or the migration locks a big table and every request times out. The fix isn't luck or a 3am maintenance window; it's a pattern: expand and contract. Here's how to change any schema with zero downtime.

Why the obvious migration takes you down

During any rolling deploy, old and new code run at the same time for a window. A migration that renames or drops a column assumes the schema and the code change atomically — they don't. The old instances hit a column that no longer exists and 500. And some operations (adding a non-null column with a default on a large table, certain index builds) take a heavy lock that blocks reads/writes while they run.

Expand and contract

The pattern decouples the schema change from the code change into backward-compatible steps. Instead of one breaking migration, you expand the schema to support both old and new, migrate the code and data, then contract by removing the old shape once nothing uses it.

  1. Expand: add the new column/table — additive only, so old code is unaffected.
  2. Backfill: copy data to the new shape in batches, not one giant locking UPDATE.
  3. Migrate code: deploy code that writes to both and reads from the new column.
  4. Contract: once no code reads the old column, drop it in a later deploy.
-- Renaming email_addr -> email, zero downtime
-- Step 1 (deploy A): add new column, no default rewrite
ALTER TABLE users ADD COLUMN email text;
-- Step 2: backfill in batches to avoid a long lock
UPDATE users SET email = email_addr WHERE id BETWEEN $1 AND $2;
-- Step 3 (deploy B): app writes both, reads `email`
-- Step 4 (deploy C): once nothing reads email_addr
ALTER TABLE users DROP COLUMN email_addr;

Every individual step is safe to run and safe to roll back on its own. That's the whole point: you never have a moment where the deployed code and the live schema disagree.

The locking traps

  • Backfill in batches with a short transaction each — a single UPDATE over millions of rows holds a lock and bloats the WAL.
  • Build indexes concurrently (CREATE INDEX CONCURRENTLY in Postgres) so writes aren't blocked.
  • Adding a NOT NULL column with a volatile default can rewrite the whole table — add nullable, backfill, then add the constraint.
  • Set a short lock_timeout so a migration that can't get its lock fails fast instead of queuing every query behind it.

Key takeaways

  • Old and new code overlap during rollout — migrations must be backward-compatible.
  • Expand → backfill → migrate code → contract, each step independently safe and reversible.
  • Backfill in batches; build indexes concurrently; avoid full-table-rewrite operations.
  • Zero-downtime schema change is a discipline, not a maintenance window.

FAQ

How do I rename a database column without downtime?

Don't rename in place. Add the new column (expand), backfill data to it in batches, deploy code that writes both and reads the new one, and only then drop the old column in a later deploy (contract). Every step is backward-compatible, so old and new code can run together during rollout.

Why did my migration lock the whole table?

Common causes are a single huge UPDATE backfill, building an index non-concurrently, or adding a NOT NULL column with a default that forces a full-table rewrite. Backfill in small batches, use CREATE INDEX CONCURRENTLY, add columns nullable then constrain, and set a short lock_timeout so a blocked migration fails fast.

AA
Ali Asghar

Senior software engineer & technical lead — 6+ years shipping production multi-tenant SaaS, payments and AI integration in Next.js, Node & TypeScript.

Keep reading
Build-log: a multi-tenant, WPS-compliant payroll system →PostgreSQL performance tuning for SaaS: the indexes that fix your slow endpoints →Payment webhooks the hard parts: idempotency, retries and reliability →