remix-run/remix · error · Error

Postgres migration lock is already held by this database

Error message

Postgres migration lock is already held by this database

What it means

withMigrationLock uses an AsyncLocalStorage-based store to detect re-entrancy: if migration code running under a lock calls withMigrationLock again on the same driver, this error is thrown. It prevents nested/duplicate advisory lock acquisition on one database connection pool.

Source

Thrown at packages/data-table-postgres/src/lib/driver.ts:377

  }

  /**
   * Runs migration work on the postgres connection that owns the advisory lock.
   *
   * Lock acquisition waits up to 60 seconds and throws when the lock cannot
   * be acquired. Re-entering this method from inside `run` throws instead of
   * deadlocking, and a failed run destroys the reserved connection instead of
   * returning it to the pool.
   * @param name Logical migration lock name.
   * @param run Migration work to run with a connection-bound driver.
   * @returns The callback result.
   */
  async withMigrationLock<result>(
    name: string,
    run: (driver: DatabaseDriver<'postgres'>) => Promise<result>,
  ): Promise<result> {
    if (this.#migrationLockStore.getStore()) {
      throw new Error('Postgres migration lock is already held by this database')
    }

    let waitForPreviousLock = this.#migrationLockQueue
    let releaseQueue: () => void = () => undefined
    this.#migrationLockQueue = new Promise((resolve) => {
      releaseQueue = resolve
    })

    await waitForPreviousLock

    try {
      let releaseOnClose = false
      let client: PostgresClient | PostgresPoolClient

      if (isPostgresPool(this.#client)) {
        client = await this.#client.connect()
        releaseOnClose = true
      } else {

View on GitHub (pinned to 9696913134)

Solutions

  1. Remove the inner withMigrationLock call — migrations run inside a lock already; keep lock acquisition in one place only
  2. If composing utilities, pass a flag/skip the lock for nested invocations
  3. Run the nested migration logic directly (without lock) inside the outer locked callback

Example fix

// before
await driver.withMigrationLock('migrate', async (d) => {
  await driver.withMigrationLock('inner', runInner) // throws
})

// after
await driver.withMigrationLock('migrate', async (d) => {
  await runInner(d)
})
Defensive patterns

Strategy: validation

Validate before calling

// Only take the lock at the top level of your migration runner
if (alreadyInsideMigration) { return run(driver) } // skip nested lock
return driver.withMigrationLock(name, run)

Prevention

When it happens

Trigger: A migration's `run` callback calling driver.withMigrationLock again (directly or via a migrator that itself wraps migrations in a lock); composing two migration utilities that both take the lock; recursive migration runners sharing the driver.

Common situations: Nesting a custom migrate() helper that already runs under withMigrationLock inside another locked migration; upgrading the migrator so it now takes the lock while app code also does; test setups that chain locked migrations on the same driver.

Related errors


AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27). Data as JSON: /api/errors/817181b2a7852ec9. Report an issue: GitHub.