awesome-everything RU
↑ Back to the climb

Databases

Advisory locks, migration tools, and deploy coordination

Crux pg_advisory_lock serialises concurrent migration runners; migration tool choice (Atlas, pgroll, Prisma, Drizzle) determines how much safety is automated vs manual.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min

A Kubernetes deployment scales from zero to three replicas simultaneously. All three pods boot and each tries to run the pending migration. Without coordination, all three apply it in parallel — one succeeds, two fail with duplicate-constraint errors, the app half-starts in an unknown state.

Postgres DDL transaction semantics

Most DDL in Postgres is transactional: BEGIN; CREATE TABLE; ALTER TABLE; COMMIT; either all succeeds or all rolls back atomically. Concurrent sessions see the change only after COMMIT (MVCC snapshot isolation).

Exceptions that cannot run inside a transaction block:

  • CREATE INDEX CONCURRENTLY
  • REINDEX CONCURRENTLY
  • ALTER TYPE ... ADD VALUE (in PG 11 and earlier; transactional from PG 12+)

Migration tools wrap each migration in a transaction by default. Any migration containing a CONCURRENT operation needs a non-transactional mode annotation. Most tools support this:

  • Atlas: -- atlas:txmode none
  • golang-migrate: -- migrate:notransaction
  • Prisma: not natively supported; use prisma db execute with a raw SQL file

If you mix a CONCURRENT operation with other DDL in one migration file, split them into two files.

The advisory lock pattern for serialised migration runners

Multiple application servers booting simultaneously can each try to run pending migrations. Without coordination, two processes might apply the same migration twice or conflict on the schema_migrations table.

The standard fix is a Postgres advisory lock at migration start:

-- Acquired before any migrations run:
SELECT pg_advisory_lock(12345);  -- 12345 = stable integer key for this app

-- Migrations run here ...

-- Released after all migrations complete:
SELECT pg_advisory_unlock(12345);

pg_advisory_lock blocks until it acquires the lock. The first server gets it and runs migrations; subsequent servers wait. When the first finishes and releases, the next server acquires — but finds all migrations already applied and does nothing.

Most migration tools implement this automatically:

  • golang-migrate: acquires pg_advisory_lock(hashInt64(databaseName)) at start.
  • Prisma: uses a _prisma_migrations table with row-level locking.
  • Atlas: uses pg_advisory_lock with a configurable key.

For non-blocking startup behaviour, use pg_try_advisory_lock with a retry loop:

-- Returns true if acquired, false if already held
SELECT pg_try_advisory_lock(12345);
Toollock_timeout supportRetriesDDL lintNon-transactional mode
AtlasYes (config)Yes (exponential backoff)Yes (built-in + Squawk)Yes (txmode none)
pgrollYesYesVia view-layer safetyN/A (handles CONCURRENT internally)
Prisma MigrateManual (SET in SQL)ManualSquawk (separate CI step)Limited
golang-migrateManual (SET in SQL)ExternalSquawk (separate CI step)Yes (notransaction)
FlywayManual (SET in SQL)ManualPro edition onlyYes

Deploy coordination: migration before pod rollout

The three common patterns:

  1. Pre-deploy job (recommended): a k8s Job or CI step runs migrate up against production before the rolling deploy. Code deploy is gated on migration success. Old pods handle traffic during migration; schema is additive (expand-only). This is the cleanest pattern — clear separation of concerns.

  2. Init container on each pod: each new pod runs migrate up on boot; advisory lock serialises them. Simple but slow — every pod waits for the migration check, even if nothing is pending.

  3. Separate migration pipeline: migration is run manually or on its own cadence, fully decoupled from code deploy. Requires human coordination; error-prone at scale.

2026 senior default: separate pre-deploy job (pattern 1), gated on migration success. The migration job emits metrics; any failure pages on-call and blocks the code rollout.

Migration tool landscape (2026)
Atlas (recommended for Postgres teams)
Declarative + lint + retries + dry-run
pgroll (zero-downtime via virtual schemas)
Views + triggers, PG 14+
Prisma Migrate (TS/Node stacks)
Schema-first, generates migration SQL
Drizzle (TS/Node, lightweight)
Schema-first, similar to Prisma
golang-migrate (language-agnostic)
Simple up/down SQL files
Squawk (linter)
CI: catches unsafe DDL in PRs
Advisory lock acquired at startup
Standard in golang-migrate, Atlas
Why this works

Why does pgroll use views instead of the direct schema? pgroll’s goal is to make a declarative migration that renames a column feel like a single-step operation to the developer. Underneath, it has to maintain compatibility for both old and new code simultaneously — views with triggers are the only Postgres-native mechanism to present two different column names backed by the same storage, without application changes to manage dual-write manually. The cost is operational complexity: debugging a migration incident requires understanding the view layer.

Quiz

Three k8s pods boot simultaneously and each tries to run pending migrations. Without an advisory lock, what is the most likely failure mode?

Quiz

Which migration tool offers built-in DDL linting, lock_timeout configuration, retries, and non-transactional mode for Postgres in 2026?

Recall before you leave
  1. 01
    How does pg_advisory_lock prevent duplicate migration execution across concurrent pods, and how do tools implement it?
  2. 02
    What is the recommended deploy coordination pattern for a k8s app, and why is it preferred over running migrations on pod startup?
  3. 03
    Why must CREATE INDEX CONCURRENTLY run outside a transaction, and how do you configure this in migration tools?
Recap

Postgres DDL is transactional — BEGIN; ALTER TABLE; COMMIT; rolls back atomically on failure — except for CREATE INDEX CONCURRENTLY and a few other operations that must run outside a transaction block and need a tool-level annotation. When multiple application pods boot simultaneously, all may try to run pending migrations; pg_advisory_lock serialises them by letting only one session run at a time. Atlas (declarative schema, built-in lint, lock_timeout, retries, dry-run) is the most complete Postgres migration tool in 2026; pgroll adds virtual-schema zero-downtime for teams with frequent breaking changes; Prisma/Drizzle provide schema-first ergonomics for TS stacks. The recommended deploy pattern is a pre-deploy migration job gated before the rolling pod deploy, with Squawk CI lint blocking unsafe PRs before merge.

Connected lessons
appears again in140
Continue the climb ↑Migration failure taxonomy and production discipline
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.