Engineering blog · 3 February 2026

Blue-green schema migrations without breaking production

Adding a column is easy. Removing one, renaming it, or changing its type while two versions of your application are live is where teams get hurt. Here is the sequence we recommend, and the two places it still goes wrong.

The rule

During a blue-green deploy both versions run against the same database. Every migration must therefore be compatible with the code that is already running and the code that is about to run. That means no destructive step ships with the deploy that needs it.

Renaming a column, in four deploys

  1. Expand. Add the new column, nullable. Backfill in batches with a bounded statement timeout.
  2. Dual-write. Ship code that writes both columns and reads the old one.
  3. Switch reads. Ship code that reads the new column. The old one is still written.
  4. Contract. Stop writing the old column, then drop it — in a later deploy, never the same one.

Where it goes wrong

1. The lock you did not think about

Even ALTER TABLE ... ADD COLUMN with a default takes an ACCESS EXCLUSIVE lock briefly. On a busy table behind a connection pool, a query already holding a lock will make your migration queue — and every request behind it. Always set lock_timeout to a couple of seconds and retry, rather than letting the migration wait indefinitely.

2. The backfill that becomes an outage

A single UPDATE across ten million rows holds row locks, bloats the table and hands your replicas a monster WAL segment to replay. Batch it, commit between batches, and watch replay_lag while it runs.

What we automate

Norvik migration runs apply lock_timeout and statement_timeout by default, refuse destructive statements unless the change is explicitly marked as a contract step, and pause automatically when replica replay lag crosses a threshold you set per project.


← Back to the blog