go 19 lines · 1 tab

Run database migrations on startup (with a guard)

Leah Thompson Jan 2026
1 tab
package migrate

import (
  "context"

  "github.com/jackc/pgx/v5/pgxpool"
)

func WithAdvisoryLock(ctx context.Context, db *pgxpool.Pool, lockID int64, fn func(context.Context) error) error {
  var got bool
  if err := db.QueryRow(ctx, `SELECT pg_try_advisory_lock($1)`, lockID).Scan(&got); err != nil {
    return err
  }
  if !got {
    return nil
  }
  defer db.Exec(ctx, `SELECT pg_advisory_unlock($1)`, lockID)
  return fn(ctx)
}
1 file · go Explain with highlit

I generally prefer running migrations in a separate release step, but for smaller deployments it’s sometimes pragmatic to run them at startup. The failure mode to avoid is “every instance runs migrations at once.” The pattern here uses Postgres advisory locks: only one instance can hold the lock, so only one instance performs migrations. Everyone else waits (or skips) and then continues booting. This keeps startup safe during rolling deploys and prevents two migrators from colliding. I still keep migrations idempotent and I make startup fail if migrations fail, because a partially migrated schema is worse than downtime. In production, you’ll want observability around migration duration and a maximum lock wait so the service doesn’t hang forever. But as a baseline, advisory locks make the approach much safer.


Related snips

Share this code

Here's the card — post it anywhere.

Run database migrations on startup (with a guard) — share card
Link copied