All posts
Internet Fundamentals

Database Migrations Without Downtime: Expand and Contract, Locks, and the Postgres Operations That Bite

By

October 26, 2026

6 min read

Most schema changes are safe; a few take locks that stop every query on a busy table. The expand-and-contract pattern for renames and type changes, the Postgres operations to treat with care (indexes, constraints, column rewrites), lock timeouts, and why the migration and the deploy must be separate steps.

Internet Fundamentals
PostgreSQL
Database Migrations
Deployments
Reliability

During a deploy, old and new versions of your code run at the same time against one database. Every schema change must work with both. That rule, more than any tool, is what makes migrations safe.

Expand and contract

  1. Expand: add the new column or table alongside the old one. Old code ignores it.

  2. Deploy code that writes to both and reads from the old.

  3. Backfill existing rows in batches.

  4. Deploy code that reads from the new.

  5. Contract: once nothing uses the old column, drop it in a later release.

Renaming a column in one step breaks whichever code version expects the other name. Expand and contract turns it into several safe steps.

Want a second opinion on this for your system?

Book a free 30-minute call. You'll leave knowing what we'd do first and roughly what it costs, whether or not you hire us.

Postgres operations to treat with care

Operation

Risk

Safer approach

CREATE INDEX

Blocks writes to the table while it builds

CREATE INDEX CONCURRENTLY (outside a transaction)

Adding a foreign key or check constraint

Scans the table while holding a lock

Add it NOT VALID, then VALIDATE CONSTRAINT separately

Changing a column's type

Often rewrites the whole table under an exclusive lock

Add a new column, backfill, switch, drop

Adding a column with a default

Fast since Postgres 11 for constant defaults; a volatile default rewrites the table

Use a constant default, or add without one and backfill

SET NOT NULL

Scans the table under lock

Add a NOT VALID check constraint, validate it, then set NOT NULL

Any DDL on a busy table

Waits behind long queries, and everything queues behind it

SET lock_timeout to a few seconds and retry

Process

  • Run migrations as their own step before the code deploy, not on application boot across many instances.

  • Generate migrations with your ORM's tool and review the SQL it produced.

  • Test them against a copy of production-sized data; a migration that takes 50 ms on a test database can take an hour on the real one.

  • Take a snapshot before anything destructive.

hoolco Monthly Review

Once a month: what changed in tech that month and what it means for you, from cloud and AI to security and the tools you build with. Every item links to its source. No sales pitch, unsubscribe in one click.

As of October 26, 2026. Provided as is, for general information, and accurate to the best of our knowledge on the date shown. Not legal, compliance, tax, financial or other professional advice. Prices, limits and vendor services change; check current vendor documentation before relying on anything here. Terms of use.