Migration Safety Rules
Migration deploy jobs must fail fast on lock contention and never block application traffic unbounded. Encode timeouts and phased DDL in team rules.
Recipe
-- First lines of every production migration file
SET lock_timeout = '5s';
SET statement_timeout = '0'; -- allow long CONCURRENTLY
SET idle_in_transaction_session_timeout = '60s';# Deploy job wrapper
flyway migrate || { echo "migration failed - app NOT rolled forward"; exit 1; }When to reach for this: CI template for SQL migrations, incident retro after lock pile-up, Flyway/Liquibase standardization.
Working Example
SET lock_timeout = '3s';
BEGIN;
CREATE TABLE app.new_feature (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL
);
COMMIT;-- Separate file: executeInTransaction=false (Flyway)
SET lock_timeout = '10s';
SET statement_timeout = '0';
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_new_feature_name
ON app.new_feature (name);Deploy pipeline rules:
- Migration failure blocks app deploy (same pipeline stage or earlier)
- Alert on
lock timeoutin migration logs - Invalid index check post-deploy:
SELECT indexrelid::regclass
FROM pg_index
WHERE NOT indisvalid;Deep Dive
Timeout Matrix
| Setting | Migration default | Purpose |
|---|---|---|
lock_timeout | 3s-10s | Abort if cannot acquire lock |
statement_timeout | 0 for CONCURRENTLY; 30s for trial DDL | Kill runaway statements |
idle_in_transaction_session_timeout | 60s | Prevent hung migrator sessions |
Lock-Aware DDL Order
ADD COLUMNnullable (fast)- Backfill (chunked, separate job)
CREATE INDEX CONCURRENTLYADD CONSTRAINT NOT VALIDVALIDATE CONSTRAINToff-peakDROP COLUMNonly after app contract phase
Deploy Job Rules
- Single migrator connection (not parallel Flyway on same DB)
- No migration during overlapping maintenance that also restarts Postgres
- Schema diff smoke test after migrate
- Roll forward documented; no history table surgery
Gotchas
- Zero lock_timeout - Migration waits behind weekend analytics query for hours. Fix: Always set explicit
lock_timeout. - Same statement_timeout for all SQL - CONCURRENTLY killed at 30s on huge table. Fix: Split files; disable txn for long runners.
- Migration success + invalid index - App queries ignore broken index, seq scan storm. Fix: Post-deploy validity query in CI.
- Deploy app before migration - Old app hits missing column. Fix: Migrate-first or expand-compatible order.
- Running migrations from laptop - Bypasses CI grants check. Fix: Pipeline-only prod migrate.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Online schema change tool (gh-ost class) | MySQL habits | Postgres native CONCURRENTLY suffices |
| Maintenance window | Tiny table startup | SLA-bound service |
| Manual DBA runbook | Regulated one-off | Continuous deploy shop |
FAQs
Retry after lock timeout?
Yes with exponential backoff in deploy job - after confirming no partial non-transactional step left INVALID objects.
lock_timeout in app code too?
Apps use shorter timeouts for queries; migrations use separate role and job - do not copy values blindly.
Who sets timeouts - DBA or dev?
DBA publishes template; dev copies into every migration file via linter or snippet.
Related
- Zero-Downtime DDL - phased DDL
- Migrations Best Practices - full list
- Rollback Strategy - forward-only ADR
Stack versions: This page was written for PostgreSQL 18.4 (stable 18, maintenance 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, and PostGIS 3.5+.